Compare commits
3 Commits
164f90d603
...
e2627c1aa4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e2627c1aa4 | ||
|
|
44f261e269 | ||
|
|
86408887e5 |
112
CLAUDE.md
112
CLAUDE.md
@@ -1,104 +1,86 @@
|
|||||||
# CLAUDE.md
|
# CLAUDE.md
|
||||||
|
|
||||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
Ghid Claude Code pentru acest repo.
|
||||||
|
|
||||||
## Ce este
|
## Ce este
|
||||||
|
|
||||||
Gateway web (Python / FastAPI) care preia prezentari de service-auto si le declara la **RAR AUTOPASS** (Legea 142/2023, OM 210/2024). Inlocuieste integrarea Visual FoxPro `RarAutoPass` (ROAAUTO), arhivata in `legacy-vfp/` (doar referinta).
|
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).
|
||||||
|
|
||||||
Limba proiectului este **romana**: cod, comentarii, commit-uri, documentatie. Fara emoji in raspunsuri sau fisiere noi (preferinta proiect).
|
## Stil
|
||||||
|
|
||||||
## Surse de adevar (citeste-le inainte sa modifici comportament)
|
- 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.
|
||||||
|
|
||||||
- `docs/api-rar-contract.md` — contractul RAR. **Unde un plan/cod difera de contract, contractul are dreptate.**
|
## Surse de adevar
|
||||||
- `docs/ROADMAP.md` — singura sursa de progres + procesul de lucru. O sesiune noua porneste de aici. Doar sectiunea "Stadiu Implementare" se modifica pe parcurs; detaliile stau in PRD-uri (`docs/prd/`).
|
|
||||||
|
- `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 in `docs/prd/`.
|
||||||
|
|
||||||
## Comenzi
|
## Comenzi
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip3 install -r requirements.txt # Python 3.12+
|
pip3 install -r requirements.txt # Python 3.12+
|
||||||
|
|
||||||
# Rulare locala (dev): API + worker sunt PROCESE SEPARATE
|
# Dev: API + worker = procese SEPARATE
|
||||||
uvicorn app.main:app --reload --port 8010 # API: dashboard /, Swagger /docs, /healthz, /metrics
|
uvicorn app.main:app --reload --port 8010 # dashboard /, /docs, /healthz, /metrics
|
||||||
python3 -m app.worker # worker (necesar doar pentru a procesa coada)
|
python3 -m app.worker # proceseaza coada
|
||||||
|
|
||||||
# Wrapper-ul start.sh ambaleaza mediu (test/prod) + rol (api/worker/both/finalizate)
|
# start.sh = mediu (test/prod) + rol (api/worker/both/finalizate); loguri in .run/
|
||||||
./start.sh test both --send # API + worker, trimite efectiv la RAR test (loguri in .run/)
|
./start.sh test both --send # trimite efectiv la RAR test
|
||||||
./start.sh test finalizate # listeaza prezentarile inregistrate la RAR (verificare independenta)
|
./start.sh test finalizate # listeaza prezentarile inregistrate la RAR
|
||||||
./start.sh status # stare procese + /healthz
|
./start.sh status | stop
|
||||||
./start.sh stop # opreste procesele pornite cu "both"
|
|
||||||
./start-test.sh / ./start-prod.sh # fixeaza mediul, forwardeaza rolul
|
./start-test.sh / ./start-prod.sh # fixeaza mediul, forwardeaza rolul
|
||||||
|
|
||||||
# Teste (pytest; folosesc FastAPI TestClient + SQLite temporar). Testele live RAR sunt
|
# Teste (TestClient + SQLite temporar); testele live RAR sunt skip implicit (marker `live`)
|
||||||
# skip implicit (marker `live`) — `pytest -q` nu atinge endpointul real.
|
|
||||||
python3 -m pytest -q
|
python3 -m pytest -q
|
||||||
python3 -m pytest tests/test_worker_reconcile.py -q # un fisier
|
python3 -m pytest tests/test_worker_reconcile.py::test_x -q
|
||||||
python3 -m pytest tests/test_worker_reconcile.py::test_x -q # un singur test
|
AUTOPASS_LIVE_RAR=1 python3 -m pytest tests/test_live_rar.py -q # LIVE opt-in: creeaza FINALIZATA; cere settings.xml cu creds <test>
|
||||||
python3 -m pytest -q -m "not live" # exclude explicit testele live
|
|
||||||
|
|
||||||
# Test LIVE pe RAR test (opt-in, skip implicit; atinge endpointul real -> creeaza FINALIZATA):
|
# Chei API (admin, doar CLI); cheia rfak_... se afiseaza O SINGURA DATA
|
||||||
# reproduce lantul mapare inline -> queued -> worker -> sent -> verificare in finalizate.
|
python3 -m tools.apikey create --account 2 | list | rotate | revoke
|
||||||
AUTOPASS_LIVE_RAR=1 python3 -m pytest tests/test_live_rar.py -q # necesita settings.xml cu creds <test>
|
|
||||||
|
|
||||||
# Lifecycle chei API (admin, doar din CLI — nu exista suprafata HTTP)
|
docker compose up --build # deploy: api + worker + autoheal, acelasi image + volum SQLite
|
||||||
python3 -m tools.apikey create --account 2 # cheie afisata O SINGURA DATA (rfak_...)
|
|
||||||
python3 -m tools.apikey list|rotate|revoke
|
|
||||||
|
|
||||||
# Docker (deploy): api + worker + autoheal, acelasi image + volum SQLite
|
|
||||||
docker compose up --build
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Arhitectura
|
## Arhitectura
|
||||||
|
|
||||||
**Doua procese peste acelasi SQLite (WAL) persistent**, care comunica EXCLUSIV prin tabela `submissions`:
|
**Doua procese peste acelasi SQLite (WAL)**, comunica EXCLUSIV prin tabela `submissions`:
|
||||||
|
|
||||||
- **API** (`app.main:app`) — API v1 (`app/api/v1/router.py` + `import_router.py`), dashboard web HTMX (`app/web/routes.py` + `templates/`), `/healthz`, `/metrics`. Worker-ul **nu** ruleaza ca task aici: un worker mort nu trebuie sa lase containerul "sanatos".
|
- **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 → recupereaza orfane → claim atomic (`BEGIN IMMEDIATE`) → login RAR (per cont) → `postPrezentare` → update. Retry/backoff exponential, reconciliere anti-duplicat, lease pe randuri `sending` orfane, re-login la JWT expirat (TTL 30h). Send DEZACTIVAT implicit (`AUTOPASS_WORKER_SEND_ENABLED=false`) — sigur pentru probe.
|
- **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 pe `sending` orfane, re-login la JWT expirat (TTL 30h). Send OFF implicit (`AUTOPASS_WORKER_SEND_ENABLED=false`).
|
||||||
|
|
||||||
**Doua canale de intrare** convergent in coada `submissions`:
|
**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`).
|
||||||
- Canal API (Treapta 1): `POST /v1/prezentari` pentru ROAAUTO / soft propriu.
|
|
||||||
- Import web (Treapta 2): upload xlsx/csv → mapare coloane → preview → commit (`app/import_parse.py`, `import_router.py`, dashboard).
|
|
||||||
|
|
||||||
Flux: validare (`validation.py`) → mapare operatie→cod (`mapping.py`) → enqueue cu PII criptat → worker trimite → dashboard monitorizeaza live.
|
Flux: validare (`validation.py`) → mapare operatie→cod (`mapping.py`) → enqueue cu PII criptat → worker trimite → dashboard live.
|
||||||
|
|
||||||
### Invariante critice (usor de stricat)
|
### Invariante critice
|
||||||
|
|
||||||
- **`AUTOPASS_CREDS_KEY` trebuie sa fie ACEEASI intre API si worker.** API cripteaza creds RAR (Fernet), worker le decripteaza. Chei diferite → worker nu poate decripta → trimiterile esueaza. `start.sh both` genereaza o cheie efemera partajata; pentru prod pune una persistenta in `.env`. (`crypto.py`)
|
- **`AUTOPASS_CREDS_KEY` identica intre API si worker** (Fernet, `crypto.py`): chei diferite → worker nu decripteaza creds → trimiterile esueaza. `start.sh both` genereaza cheie efemera partajata; in prod, cheie persistenta in `.env`.
|
||||||
- **Idempotenta = hash de continut canonic** server-side (`idempotency.py`), pentru ca RAR accepta duplicate si nu are nr. comanda. `build_key` normalizeaza INTOTDEAUNA `account_id` la `account_or_default` (None == 1) INAINTE de hash — altfel acelasi rand logic primeste chei diferite pe canalele API vs import (OV-2). `canonicalize_row` normeaza VIN/nr/odometru (strip ".0" din coercion Excel) inainte de validare si de cheie.
|
- **Idempotenta = hash de continut canonic** server-side (`idempotency.py`) — RAR accepta duplicate, fara nr. comanda. `build_key` normalizeaza `account_id` la `account_or_default` (None == 1) INAINTE de hash, altfel acelasi rand primeste chei diferite pe API vs import. `canonicalize_row` normeaza VIN/nr/odometru (strip ".0" din Excel) inainte de validare si cheie.
|
||||||
- **`FINALIZATA` e terminal la RAR** — fara anulare/corectie prin API. De aceea reconcilierea anti-duplicat: pe eroare **ambigua** (timeout / TransportError / 502/503/504 / 429 / 408) sau rand `sending` orfan, worker-ul cauta in finalizate (match pe vin+dataPrestatie+odometruFinal) si marcheaza `sent` fara a re-trimite (`reconcile.py`). **EXCEPTIE: un RAR 500 cu mesaj** (`RarError.rar_message`, ex. `ORA-12899`) e un esec DEFINITIV (RAR a raspuns „am esuat", nu o pierdere de raspuns) → worker-ul NU reconciliaza si NU reincearca, marcheaza `error` cu mesajul RAR (`RAR_EROARE_SERVER`). Altfel ar marca fals `sent` pe un record PARTIAL pe care RAR (ne-tranzactional) il lasa la esec.
|
- **`FINALIZATA` e terminal la RAR** (fara anulare/corectie). Pe eroare **ambigua** (timeout/TransportError/502/503/504/429/408) sau `sending` orfan, worker-ul cauta in finalizate (vin+dataPrestatie+odometruFinal) si marcheaza `sent` fara re-trimitere (`reconcile.py`). **EXCEPTIE: RAR 500 cu mesaj** (`RarError.rar_message`, ex. `ORA-12899`) = esec DEFINITIV → fara reconciliere/retry, `error` cu mesajul RAR (`RAR_EROARE_SERVER`); altfel ar marca fals `sent` un record PARTIAL lasat de RAR (ne-tranzactional).
|
||||||
- **Creds RAR per cont**: durabile in `accounts.rar_creds_enc` (canal web, fallback re-login) SAU efemere in `submissions.rar_creds_enc` (canal API, sterse dupa primul login reusit). Worker incearca submission-ul intai, apoi fallback la cont. Purjarea sterge DOAR `submissions.rar_creds_enc`, NU `accounts.rar_creds_enc`.
|
- **Creds RAR per cont**: durabile in `accounts` (canal web, fallback re-login) SAU efemere in `submissions.rar_creds_enc` (canal API, sterse dupa primul login reusit). Worker incearca submission-ul intai, apoi contul. Purjarea sterge DOAR creds efemere din `submissions`.
|
||||||
- **Auth API-key** (`auth.py`): identifica CONTUL ROAAUTO, separat de credentialele RAR. Stocam doar SHA-256 al cheii. Enforcement prin `AUTOPASS_REQUIRE_API_KEY`: `false` (dev) → fara cheie merge pe cont id=1, cheie invalida → 401; `true` (prod) → cheie obligatorie pe `/v1/*` protejat. POST-urile + rutele de import sunt account-scoped; GET-urile de listare sunt si ele account-scoped (5.15/US-011: fragmentele `_fragments/submissions|trimitere|mapari|status|jurnal|nomenclator|trimiteri-versiune` sub `require_login` + scope, 404-before-leak pe id strain; `GET /v1/prezentari(/{id})`/`/v1/mapari`/`/v1/audit/export` filtrate pe cont). `GET /v1/nomenclator` ramane public intentionat (coduri RAR publice, fara PII).
|
- **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/nomenclator` public intentionat (coduri RAR publice, fara PII).
|
||||||
- **Admin bootstrap fara hardcodare**: la `/signup`, primul user din TOATA baza (`count_admins()==0`, verificat in tranzactie) primeste `is_admin=1` automat; orice signup ulterior e neadmin. `is_admin` da acces la `/admin` (panou global, toate conturile) — verificat pe `account_id`-ul sesiunii, dar neconditionat DE care `account_id` a primit userul (semnul de "cine e admin" nu are legatura cu contul `id=1`). Contul `id=1` e doar bucket-ul de fallback pentru trafic API neautentificat in dev (`AUTOPASS_REQUIRE_API_KEY=false`) — separat de conceptul de admin. Fix manual daca primul signup s-a consumat deja gresit: `python3 -m tools.account set-admin --account N`.
|
- **Admin bootstrap**: primul user din TOATA baza la `/signup` (`count_admins()==0`, in tranzactie) devine `is_admin=1`; restul nu. `is_admin` da 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 retinuta per `(account_id, signature_coloane)`** (`column_mappings`): la urmatorul fisier cu aceleasi coloane, pentru acelasi cont, maparea se reaplica automat. Un cont poate avea mai multe formate memorate simultan.
|
- **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**: prestatie poate veni cu `cod_prestatie` (cod RAR direct) sau `cod_op_service` (cod intern) + `denumire`. Nerezolvat → submission `needs_mapping` (nu se trimite), apare in editorul web cu sugestie fuzzy; la salvarea maparii se re-rezolva automat submission-urile blocate.
|
- **Mapare operatie→cod**: prestatia vine cu `cod_prestatie` (cod RAR) sau `cod_op_service` + `denumire`. Nerezolvat → `needs_mapping` (nu se trimite), editor web cu sugestie fuzzy; salvarea maparii re-rezolva automat submission-urile blocate.
|
||||||
- **`cod_prestatie` e VALIDAT fata de nomenclator la ingestie** (`resolve_prestatii(..., valid_codes)`): un cod direct NECUNOSCUT in nomenclator NU se mai trimite raw — e promovat la `cod_op_service` (denumire=cod) si tratat ca operatie de mapat. Motiv (confirmat live 2026-06-23): RAR accepta NUMAI coduri din nomenclator (coloana `COD_PRESTATIE` max 5 car.); un cod necunoscut da **HTTP 500** (`ORA-12899`), iar RAR **NU e tranzactional** → lasa un record PARTIAL `FINALIZATA` (terminal) chiar pe esec, pe care reconcilierea worker-ului l-ar marca fals `sent`. Comportamentul la cod necunoscut/nemapat: `on_unmapped_error` (camp boolean top-level pe `POST /v1/prezentari` + `/valideaza`) = `false` (intra in editor, `needs_mapping`) sau `true` (respinge fara enqueue → `submission_id=null` + `erori`). Default = `accounts.on_unmapped_error_default` (implicit `false`/`0`); precedenta cerere > cont > `false`.
|
- **`cod_prestatie` VALIDAT fata de nomenclator la ingestie** (`resolve_prestatii(..., valid_codes)`): cod necunoscut NU se trimite raw — e promovat la `cod_op_service` (denumire=cod) si intra la mapat. Motiv (confirmat live): RAR accepta doar coduri din nomenclator (max 5 car.); cod necunoscut → HTTP 500 `ORA-12899`, iar RAR ne-tranzactional lasa record PARTIAL `FINALIZATA` pe care reconcilierea l-ar marca fals `sent`. La cod nemapat: `on_unmapped_error` (boolean top-level pe `POST /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** — toate apelurile httpx trimit `User-Agent: Mozilla/5.0` (`config.py`, confirmat live).
|
- **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 global de validare in `main.py` pastreaza type/loc/msg dar DROP-a `input`/`ctx` (altfel ar reflecta `rar_credentials.password`).
|
- **422 fara echo de credentiale**: handler-ul din `main.py` pastreaza type/loc/msg dar DROP-a `input`/`ctx` (altfel reflecta `rar_credentials.password`).
|
||||||
- **Retentie**: `submissions` sent + `import_batches` primesc `purge_after = now + 90 zile`; worker-ul purjeaza odata pe ora (T16, GDPR/L.142).
|
- **Retentie**: `submissions` sent + `import_batches` → `purge_after = now + 90 zile`; worker purjeaza orar (GDPR/L.142).
|
||||||
|
|
||||||
### Masina de stari submissions
|
### Masina de stari submissions
|
||||||
|
|
||||||
`queued → sending → sent` (succes, cu `id_prezentare` de la RAR). Ramuri: `needs_mapping` (cod nerezolvat), `needs_data` (RAR 400, validare continut), `error` (max retries / 4xx nerecuperabil / **RAR 500 cu mesaj — esec definitiv** / creds invalide / login 401 — NU se face retry pe creds gresite). Backoff: `next_attempt_at = now + base*2^retry`, plafonat. Schema completa: `app/schema.sql`.
|
`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
|
## Mod non-interactiv
|
||||||
|
|
||||||
Vezi `/workspace/CLAUDE.md` (workspace-level): cand esti lansat cu `claude -p`, creeaza fisiere noi DOAR in `/workspace/.claude-work/<task>/`, nu in `/workspace`. Modificarile la fisiere existente se fac in locatia originala.
|
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
|
## Skill routing
|
||||||
|
|
||||||
When the user's request matches an available skill, invoke it via the Skill tool. When in doubt, invoke the skill.
|
Cand cererea se potriveste unui skill, invoca-l prin Skill tool; la indoiala, invoca-l.
|
||||||
|
|
||||||
Key routing rules:
|
- Idei produs → /office-hours · Strategie → /plan-ceo-review · Arhitectura → /plan-eng-review
|
||||||
- Product ideas/brainstorming → invoke /office-hours
|
- Design system/plan → /design-consultation, /plan-design-review · Pipeline complet → /autoplan
|
||||||
- Strategy/scope → invoke /plan-ceo-review
|
- Bug-uri → /investigate · QA site → /qa, /qa-only · Code review → /review · Polish vizual → /design-review
|
||||||
- Architecture → invoke /plan-eng-review
|
- Ship/deploy/PR → /ship, /land-and-deploy · Salvare/reluare context → /context-save, /context-restore · Spec → /spec
|
||||||
- Design system/plan review → invoke /design-consultation or /plan-design-review
|
|
||||||
- Full review pipeline → invoke /autoplan
|
|
||||||
- Bugs/errors → invoke /investigate
|
|
||||||
- QA/testing site behavior → invoke /qa or /qa-only
|
|
||||||
- Code review/diff check → invoke /review
|
|
||||||
- Visual polish → invoke /design-review
|
|
||||||
- Ship/deploy/PR → invoke /ship or /land-and-deploy
|
|
||||||
- Save progress → invoke /context-save
|
|
||||||
- Resume context → invoke /context-restore
|
|
||||||
- Author a backlog-ready spec/issue → invoke /spec
|
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ def create_account(
|
|||||||
# Planul cerut: pastram doar valori valide; orice altceva -> NULL (defensiv).
|
# Planul cerut: pastram doar valori valide; orice altceva -> NULL (defensiv).
|
||||||
req_plan = requested_plan if requested_plan in VALID_TIERS else None
|
req_plan = requested_plan if requested_plan in VALID_TIERS else None
|
||||||
try:
|
try:
|
||||||
# Trial Pro automat la creare (PRD 5.17 US-001): tier='free' + trial_until=now+30z.
|
# Trial Pro automat la creare: tier='free' + trial_until=now+30z.
|
||||||
trial_until = (
|
trial_until = (
|
||||||
(datetime.now(timezone.utc) + timedelta(days=30)).strftime("%Y-%m-%d %H:%M:%S")
|
(datetime.now(timezone.utc) + timedelta(days=30)).strftime("%Y-%m-%d %H:%M:%S")
|
||||||
)
|
)
|
||||||
@@ -151,7 +151,7 @@ def set_status(conn: sqlite3.Connection, account_id: int, status: str) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def get_auto_send(conn: sqlite3.Connection, account_id: int) -> bool:
|
def get_auto_send(conn: sqlite3.Connection, account_id: int) -> bool:
|
||||||
"""Comutatorul Auto per cont (PRD 5.19 US-001): `accounts.auto_send_enabled` ca bool.
|
"""Comutatorul Auto per cont: `accounts.auto_send_enabled` ca bool.
|
||||||
|
|
||||||
Cont inexistent -> False (Auto OFF: sigur, tine randurile). Nu comita (ca set_active).
|
Cont inexistent -> False (Auto OFF: sigur, tine randurile). Nu comita (ca set_active).
|
||||||
"""
|
"""
|
||||||
@@ -164,7 +164,7 @@ def get_auto_send(conn: sqlite3.Connection, account_id: int) -> bool:
|
|||||||
|
|
||||||
|
|
||||||
def set_auto_send(conn: sqlite3.Connection, account_id: int, enabled: bool) -> None:
|
def set_auto_send(conn: sqlite3.Connection, account_id: int, enabled: bool) -> None:
|
||||||
"""Comuta `accounts.auto_send_enabled` (PRD 5.19 US-001). Idempotent (set pe aceeasi
|
"""Comuta `accounts.auto_send_enabled`. Idempotent (set pe aceeasi
|
||||||
valoare nu arunca). Scoped pe cont. Nu comita — respecta conventia fisierului (apelantul
|
valoare nu arunca). Scoped pe cont. Nu comita — respecta conventia fisierului (apelantul
|
||||||
comita, ca la set_active/set_status). Cont inexistent -> UPDATE fara efect (no-op sigur).
|
comita, ca la set_active/set_status). Cont inexistent -> UPDATE fara efect (no-op sigur).
|
||||||
"""
|
"""
|
||||||
@@ -175,8 +175,8 @@ def set_auto_send(conn: sqlite3.Connection, account_id: int, enabled: bool) -> N
|
|||||||
|
|
||||||
|
|
||||||
def held_for_account(conn: sqlite3.Connection, account_id: int) -> int:
|
def held_for_account(conn: sqlite3.Connection, account_id: int) -> int:
|
||||||
"""Snapshot-ul `held` la ingestie (PRD 5.19 US-001). SURSA UNICA (chokepoint) folosita de
|
"""Snapshot-ul `held` la ingestie. SURSA UNICA (chokepoint) folosita de
|
||||||
TOATE situ-rile de ingestie care scriu `status='queued'` (US-002).
|
TOATE situ-rile de ingestie care scriu `status='queued'`.
|
||||||
|
|
||||||
Intoarce 0 (nu tine) daca `accounts.auto_send_enabled=1`, altfel 1 (tine randul). Cont
|
Intoarce 0 (nu tine) daca `accounts.auto_send_enabled=1`, altfel 1 (tine randul). Cont
|
||||||
inexistent -> 1 (sigur: tine randul, nu-l trimite fara comutator explicit).
|
inexistent -> 1 (sigur: tine randul, nu-l trimite fara comutator explicit).
|
||||||
@@ -215,7 +215,7 @@ def set_tier(
|
|||||||
"UPDATE accounts SET tier=?, trial_until=? WHERE id=?",
|
"UPDATE accounts SET tier=?, trial_until=? WHERE id=?",
|
||||||
(tier, trial_until, account_id),
|
(tier, trial_until, account_id),
|
||||||
)
|
)
|
||||||
# Audit in app_events (decizie PRD 5.17 US-008, fara PII nou)
|
# Audit in app_events (fara PII nou)
|
||||||
try:
|
try:
|
||||||
from .observ import log_event
|
from .observ import log_event
|
||||||
log_event(
|
log_event(
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Router import Treapta 2 — upload fisier xlsx/csv, preview, commit, export.
|
"""Router import — upload fisier xlsx/csv, preview, commit, export.
|
||||||
|
|
||||||
Endpointuri:
|
Endpointuri:
|
||||||
POST /v1/import — upload fisier, staging in import_batches/import_rows
|
POST /v1/import — upload fisier, staging in import_batches/import_rows
|
||||||
@@ -190,7 +190,7 @@ def _resolve_row_for_preview(
|
|||||||
denumire = str(denumire_val).strip() if denumire_val not in (None, "") else str(operatie_val)
|
denumire = str(denumire_val).strip() if denumire_val not in (None, "") else str(operatie_val)
|
||||||
mapped["prestatii"] = [{"cod_op_service": str(operatie_val), "denumire": denumire}]
|
mapped["prestatii"] = [{"cod_op_service": str(operatie_val), "denumire": denumire}]
|
||||||
|
|
||||||
# obs derive-on-empty (D7/E3 PRD 5.15): daca obs e gol si avem operatie,
|
# obs derive-on-empty: daca obs e gol si avem operatie,
|
||||||
# copiem denumirea operatiei in obs (nu o mutam — op_service ramane neatins).
|
# copiem denumirea operatiei in obs (nu o mutam — op_service ramane neatins).
|
||||||
# DERIVE-ON-EMPTY: doar cand obs e gol, ca sa fie idempotent la re-preview/re-editare.
|
# DERIVE-ON-EMPTY: doar cand obs e gol, ca sa fie idempotent la re-preview/re-editare.
|
||||||
obs_curent = str(mapped.get("obs") or "").strip()
|
obs_curent = str(mapped.get("obs") or "").strip()
|
||||||
@@ -236,7 +236,7 @@ def _resolve_row_for_preview(
|
|||||||
if all_flags and not reviewed:
|
if all_flags and not reviewed:
|
||||||
# needs_review: validarea a trecut, dar flagurile (date ambigue, formule) cer confirmare manuala.
|
# needs_review: validarea a trecut, dar flagurile (date ambigue, formule) cer confirmare manuala.
|
||||||
# Daca reviewed=True (operatorul a confirmat explicit valorile in modal), sarim
|
# Daca reviewed=True (operatorul a confirmat explicit valorile in modal), sarim
|
||||||
# acest return si continuam spre ok/needs_data (US-007, PRD 5.12).
|
# acest return si continuam spre ok/needs_data.
|
||||||
return {
|
return {
|
||||||
"resolved_status": "needs_review",
|
"resolved_status": "needs_review",
|
||||||
"resolved": mapped,
|
"resolved": mapped,
|
||||||
@@ -244,8 +244,6 @@ def _resolve_row_for_preview(
|
|||||||
"flags": all_flags,
|
"flags": all_flags,
|
||||||
}
|
}
|
||||||
|
|
||||||
# US-001 (PRD 5.11): ramura auto_send eliminata din preview.
|
|
||||||
|
|
||||||
if errors:
|
if errors:
|
||||||
return {
|
return {
|
||||||
"resolved_status": "needs_data",
|
"resolved_status": "needs_data",
|
||||||
@@ -361,7 +359,7 @@ def apply_row_override(
|
|||||||
|
|
||||||
new_override = _merge_override(current, fields)
|
new_override = _merge_override(current, fields)
|
||||||
enc = encrypt_creds(new_override) if new_override else None
|
enc = encrypt_creds(new_override) if new_override else None
|
||||||
# D#9 (PRD 5.12): resetam reviewed=0 la orice schimbare de valoare — operatorul
|
# Resetam reviewed=0 la orice schimbare de valoare — operatorul
|
||||||
# trebuie sa reconfirme dupa editare. NU conditionam pe reviewed curent: orice override
|
# trebuie sa reconfirme dupa editare. NU conditionam pe reviewed curent: orice override
|
||||||
# (chiar si revert la valoarea initiala) anuleaza confirmarea implicita.
|
# (chiar si revert la valoarea initiala) anuleaza confirmarea implicita.
|
||||||
conn.execute("UPDATE import_rows SET override_json=?, reviewed=0 WHERE id=?", (enc, row["rid"]))
|
conn.execute("UPDATE import_rows SET override_json=?, reviewed=0 WHERE id=?", (enc, row["rid"]))
|
||||||
@@ -965,7 +963,7 @@ def commit_import(
|
|||||||
if batch["status"] == "committed":
|
if batch["status"] == "committed":
|
||||||
raise HTTPException(status_code=409, detail="batch deja comis")
|
raise HTTPException(status_code=409, detail="batch deja comis")
|
||||||
|
|
||||||
# D#8 (PRD 5.12): gate commit derivat din DB `reviewed` pe AMBELE canale.
|
# Gate commit derivat din DB `reviewed` pe AMBELE canale.
|
||||||
# API: reviewed_rows pastrat (contract stabil) dar seteaza reviewed=1 in DB inainte
|
# API: reviewed_rows pastrat (contract stabil) dar seteaza reviewed=1 in DB inainte
|
||||||
# de interogare. Randurile needs_review cu reviewed=1 sunt incluse in comit.
|
# de interogare. Randurile needs_review cu reviewed=1 sunt incluse in comit.
|
||||||
if req.reviewed_rows:
|
if req.reviewed_rows:
|
||||||
@@ -1032,7 +1030,7 @@ def commit_import(
|
|||||||
if n_total_ok == 0:
|
if n_total_ok == 0:
|
||||||
raise HTTPException(status_code=422, detail="Niciun rand ok de confirmat.")
|
raise HTTPException(status_code=422, detail="Niciun rand ok de confirmat.")
|
||||||
|
|
||||||
# Rezolva mediul RAR tinta al lotului (US-009): cerut > default cont > ancora globala.
|
# Rezolva mediul RAR tinta al lotului: cerut > default cont > ancora globala.
|
||||||
try:
|
try:
|
||||||
env = rezolva_rar_env(conn, account_id, req.rar_env)
|
env = rezolva_rar_env(conn, account_id, req.rar_env)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
@@ -1044,8 +1042,8 @@ def commit_import(
|
|||||||
"disponibile": e.disponibile,
|
"disponibile": e.disponibile,
|
||||||
})
|
})
|
||||||
|
|
||||||
# T3 (PRD 5.17): enforce volum plan — INAINTE de enqueue (invariant idempotenta).
|
# Enforce volum plan — INAINTE de enqueue (invariant idempotenta).
|
||||||
# Decizie #21: respingere TOTALA a lotului (nu enqueue partial tacut).
|
# Respingere TOTALA a lotului (nu enqueue partial tacut).
|
||||||
from ...config import get_settings as _get_settings
|
from ...config import get_settings as _get_settings
|
||||||
from ...plans import PLANS, effective_tier, monthly_usage
|
from ...plans import PLANS, effective_tier, monthly_usage
|
||||||
from ...observ import log_event as _log_event_plan
|
from ...observ import log_event as _log_event_plan
|
||||||
@@ -1126,7 +1124,7 @@ def commit_import(
|
|||||||
conn.execute("BEGIN IMMEDIATE")
|
conn.execute("BEGIN IMMEDIATE")
|
||||||
try:
|
try:
|
||||||
purge_after_sql = "datetime('now', '+90 days')"
|
purge_after_sql = "datetime('now', '+90 days')"
|
||||||
# PRD 5.19 US-002: snapshot `held` din comutatorul contului (chokepoint unic
|
# Snapshot `held` din comutatorul contului (chokepoint unic
|
||||||
# held_for_account) — 0 daca Auto ON, 1 daca Auto OFF. Per-cont -> o data.
|
# held_for_account) — 0 daca Auto ON, 1 daca Auto OFF. Per-cont -> o data.
|
||||||
held_val = held_for_account(conn, acct)
|
held_val = held_for_account(conn, acct)
|
||||||
|
|
||||||
@@ -1162,7 +1160,7 @@ def commit_import(
|
|||||||
denumire = str(denumire_val).strip() if denumire_val not in (None, "") else str(operatie_val)
|
denumire = str(denumire_val).strip() if denumire_val not in (None, "") else str(operatie_val)
|
||||||
mapped["prestatii"] = [{"cod_op_service": str(operatie_val), "denumire": denumire}]
|
mapped["prestatii"] = [{"cod_op_service": str(operatie_val), "denumire": denumire}]
|
||||||
|
|
||||||
# obs derive-on-empty (D7/E3 PRD 5.15): copiere denumire in obs daca obs e gol.
|
# obs derive-on-empty: copiere denumire in obs daca obs e gol.
|
||||||
# Identic cu logica din _resolve_row_for_preview (override aplicat tot ultimul).
|
# Identic cu logica din _resolve_row_for_preview (override aplicat tot ultimul).
|
||||||
obs_curent = str(mapped.get("obs") or "").strip()
|
obs_curent = str(mapped.get("obs") or "").strip()
|
||||||
if not obs_curent and operatie_val:
|
if not obs_curent and operatie_val:
|
||||||
@@ -1272,7 +1270,7 @@ def commit_import(
|
|||||||
class RandEditIn(BaseModel):
|
class RandEditIn(BaseModel):
|
||||||
"""Campuri de continut editabile in preview. None = ne-trimis (neschimbat);
|
"""Campuri de continut editabile in preview. None = ne-trimis (neschimbat);
|
||||||
"" = sterge override-ul (revine la valoarea din fisier).
|
"" = sterge override-ul (revine la valoarea din fisier).
|
||||||
obs = text liber fara validare de continut (US-005 PRD 5.15)."""
|
obs = text liber fara validare de continut."""
|
||||||
vin: str | None = None
|
vin: str | None = None
|
||||||
nr_inmatriculare: str | None = None
|
nr_inmatriculare: str | None = None
|
||||||
data_prestatie: str | None = None
|
data_prestatie: str | None = None
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ def ping(
|
|||||||
conn.close()
|
conn.close()
|
||||||
autentificat_cu_cheie = acct is not None
|
autentificat_cu_cheie = acct is not None
|
||||||
|
|
||||||
# Verificam daca contul are creds RAR stocate (per-env, US-013).
|
# Verificam daca contul are creds RAR stocate (per mediu).
|
||||||
aid = account_or_default(account_id)
|
aid = account_or_default(account_id)
|
||||||
conn = get_connection()
|
conn = get_connection()
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ def _erori_nemapate(unmapped: list[dict]) -> list[dict]:
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
# PRD 5.19 US-010: mesaj uman pentru un rand tinut manual (queued AND held=1).
|
# Mesaj uman pentru un rand tinut manual (queued AND held=1).
|
||||||
# Reutilizeaza vocabularul existent AUTO_SEND_OPRIT (errors.py) — nu inventam al treilea
|
# Reutilizeaza vocabularul existent AUTO_SEND_OPRIT (errors.py) — nu inventam al treilea
|
||||||
# vocabular "auto_send". Onestitate: raspunsul NU trebuie sa arate un "queued" curat fals.
|
# vocabular "auto_send". Onestitate: raspunsul NU trebuie sa arate un "queued" curat fals.
|
||||||
MOTIV_HELD = (
|
MOTIV_HELD = (
|
||||||
@@ -107,8 +107,7 @@ def _motiv_clasificare(cl: dict) -> str | None:
|
|||||||
|
|
||||||
None cand status='queued'. Acopera ramurile de blocaj: erori de continut
|
None cand status='queued'. Acopera ramurile de blocaj: erori de continut
|
||||||
(needs_data) si coduri fara mapare RAR (needs_mapping).
|
(needs_data) si coduri fara mapare RAR (needs_mapping).
|
||||||
Dupa US-001: needs_mapping apare EXCLUSIV cand unmapped e non-gol
|
needs_mapping apare EXCLUSIV cand unmapped e non-gol.
|
||||||
(ramura auto_send_oprit era inaccesibila si a fost eliminata).
|
|
||||||
"""
|
"""
|
||||||
if cl["status"] == "queued":
|
if cl["status"] == "queued":
|
||||||
return None
|
return None
|
||||||
@@ -127,7 +126,7 @@ def _rezultat_enqueue(submission_id: int | None, cl: dict, held: int = 0, **extr
|
|||||||
|
|
||||||
Populeaza erori (validare continut), nemapate (coduri fara mapare) si motiv (uman)
|
Populeaza erori (validare continut), nemapate (coduri fara mapare) si motiv (uman)
|
||||||
pentru orice status != 'queued'. Aditiv: pe 'queued' toate raman goale/None, EXCEPTAND
|
pentru orice status != 'queued'. Aditiv: pe 'queued' toate raman goale/None, EXCEPTAND
|
||||||
randurile tinute manual (PRD 5.19 US-010: `held=1` -> held=true + motiv non-null).
|
randurile tinute manual (`held=1` -> held=true + motiv non-null).
|
||||||
"""
|
"""
|
||||||
tinut = bool(held)
|
tinut = bool(held)
|
||||||
motiv = _motiv_clasificare(cl) or _motiv_held(cl["status"], tinut)
|
motiv = _motiv_clasificare(cl) or _motiv_held(cl["status"], tinut)
|
||||||
@@ -179,7 +178,7 @@ def create_prezentari(
|
|||||||
conn = get_connection()
|
conn = get_connection()
|
||||||
results: list[SubmissionResult] = []
|
results: list[SubmissionResult] = []
|
||||||
try:
|
try:
|
||||||
# load_mapping_meta incarca maparea op->cod RAR; dupa US-001, auto_send nu mai tine randuri.
|
# load_mapping_meta incarca maparea op->cod RAR; auto_send nu mai tine randuri.
|
||||||
mapping_meta = load_mapping_meta(conn, acct)
|
mapping_meta = load_mapping_meta(conn, acct)
|
||||||
mapping = {op: meta["cod_prestatie"] for op, meta in mapping_meta.items()}
|
mapping = {op: meta["cod_prestatie"] for op, meta in mapping_meta.items()}
|
||||||
# Validare cod_prestatie fata de nomenclator + modul la cod necunoscut/nemapat.
|
# Validare cod_prestatie fata de nomenclator + modul la cod necunoscut/nemapat.
|
||||||
@@ -189,12 +188,12 @@ def create_prezentari(
|
|||||||
text_rules = load_text_rules(conn, acct)
|
text_rules = load_text_rules(conn, acct)
|
||||||
error_mode = _effective_on_unmapped_error(conn, acct, req.on_unmapped_error)
|
error_mode = _effective_on_unmapped_error(conn, acct, req.on_unmapped_error)
|
||||||
|
|
||||||
# US-005: rezolva mediul RAR tinta (cerut > default cont > ancora globala).
|
# Rezolva mediul RAR tinta (cerut > default cont > ancora globala).
|
||||||
# MediuIndisponibil -> 422 inainte de orice enqueue (respinge tot lotul).
|
# MediuIndisponibil -> 422 inainte de orice enqueue (respinge tot lotul).
|
||||||
try:
|
try:
|
||||||
env = rezolva_rar_env(conn, acct, req.rar_env)
|
env = rezolva_rar_env(conn, acct, req.rar_env)
|
||||||
except MediuIndisponibil as e:
|
except MediuIndisponibil as e:
|
||||||
# US-012: audit blocare mediu indisponibil (tip='rar_env_blocat').
|
# Audit blocare mediu indisponibil (tip='rar_env_blocat').
|
||||||
log_event(
|
log_event(
|
||||||
"rar_env_blocat",
|
"rar_env_blocat",
|
||||||
nivel="WARNING",
|
nivel="WARNING",
|
||||||
@@ -220,8 +219,8 @@ def create_prezentari(
|
|||||||
detail=err_eroare("RAR_MEDIU_INDISPONIBIL", cauza="valoare invalida pentru rar_env"),
|
detail=err_eroare("RAR_MEDIU_INDISPONIBIL", cauza="valoare invalida pentru rar_env"),
|
||||||
)
|
)
|
||||||
|
|
||||||
# T3 (PRD 5.17): enforce volum plan — INAINTE de build_key/enqueue (invariant idempotenta).
|
# Enforce volum plan — INAINTE de build_key/enqueue (invariant idempotenta).
|
||||||
# Decizie #21: respingere TOTALA a lotului (nu enqueue partial tacut).
|
# Respingere TOTALA a lotului (nu enqueue partial tacut).
|
||||||
from ...config import get_settings as _get_settings
|
from ...config import get_settings as _get_settings
|
||||||
from ...plans import PLANS, effective_tier, monthly_usage
|
from ...plans import PLANS, effective_tier, monthly_usage
|
||||||
_settings = _get_settings()
|
_settings = _get_settings()
|
||||||
@@ -259,8 +258,8 @@ def create_prezentari(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
# PRD 5.19 US-002: snapshot `held` din comutatorul contului (chokepoint unic
|
# Snapshot `held` din comutatorul contului (chokepoint unic held_for_account).
|
||||||
# held_for_account). 0 daca Auto ON, 1 daca Auto OFF (tine randul). Depinde de
|
# 0 daca Auto ON, 1 daca Auto OFF (tine randul). Depinde de
|
||||||
# cont, nu de rand -> calculat o data. NU intra in payload/idempotenta.
|
# cont, nu de rand -> calculat o data. NU intra in payload/idempotenta.
|
||||||
held_val = held_for_account(conn, acct)
|
held_val = held_for_account(conn, acct)
|
||||||
|
|
||||||
@@ -291,9 +290,8 @@ def create_prezentari(
|
|||||||
# on_unmapped_error=True: nu reactivam; randul ramane 'error'.
|
# on_unmapped_error=True: nu reactivam; randul ramane 'error'.
|
||||||
results.append(_rezultat_respins(existing["id"], cl, rar_env=env))
|
results.append(_rezultat_respins(existing["id"], cl, rar_env=env))
|
||||||
continue
|
continue
|
||||||
# PRD 5.19 US-002 (Eng Finding A — bug de bypass): la reactivare
|
# La reactivare re-snapshot-am `held` din comutatorul contului. Fara
|
||||||
# re-snapshot-am `held` din comutatorul contului. Fara asta randul
|
# asta randul pastra held VECHI si se auto-trimitea desi contul e Auto OFF.
|
||||||
# pastra held VECHI si se auto-trimitea desi contul e Auto OFF.
|
|
||||||
cur = conn.execute(
|
cur = conn.execute(
|
||||||
"UPDATE submissions SET status=?, payload_json=?, rar_error=?, "
|
"UPDATE submissions SET status=?, payload_json=?, rar_error=?, "
|
||||||
"rar_creds_enc=COALESCE(?, rar_creds_enc), retry_count=0, "
|
"rar_creds_enc=COALESCE(?, rar_creds_enc), retry_count=0, "
|
||||||
@@ -304,7 +302,7 @@ def create_prezentari(
|
|||||||
)
|
)
|
||||||
if cur.rowcount == 1:
|
if cur.rowcount == 1:
|
||||||
# Creds noi se propaga si in slotul durabil per-env al contului
|
# Creds noi se propaga si in slotul durabil per-env al contului
|
||||||
# — ambele canale converg pe parola corectata (US-013, env-aware).
|
# — ambele canale converg pe parola corectata (env-aware).
|
||||||
if req.rar_credentials is not None:
|
if req.rar_credentials is not None:
|
||||||
conn.execute(
|
conn.execute(
|
||||||
f"UPDATE accounts SET rar_creds_{env}_enc=?, rar_{env}_enabled=1 WHERE id=?",
|
f"UPDATE accounts SET rar_creds_{env}_enc=?, rar_{env}_enabled=1 WHERE id=?",
|
||||||
@@ -321,7 +319,7 @@ def create_prezentari(
|
|||||||
"SELECT id, status, id_prezentare, held FROM submissions WHERE id=?",
|
"SELECT id, status, id_prezentare, held FROM submissions WHERE id=?",
|
||||||
(existing["id"],),
|
(existing["id"],),
|
||||||
).fetchone()
|
).fetchone()
|
||||||
# Echo onest pe dedup (PRD 5.19 US-002/US-010): propaga `held` al randului
|
# Echo onest pe dedup: propaga `held` al randului
|
||||||
# existent — un rand tinut NU trebuie sa apara ca un "queued" curat fals.
|
# existent — un rand tinut NU trebuie sa apara ca un "queued" curat fals.
|
||||||
dedup_held = bool(existing["held"])
|
dedup_held = bool(existing["held"])
|
||||||
results.append(
|
results.append(
|
||||||
@@ -338,13 +336,13 @@ def create_prezentari(
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# Helper pur partajat cu dry-run: reproduce EXACT clasificarea
|
# Helper pur partajat cu dry-run: reproduce EXACT clasificarea
|
||||||
# (canonicalize + mapare op->cod + validare; auto_send gate eliminat dupa US-001).
|
# (canonicalize + mapare op->cod + validare; fara gate auto_send).
|
||||||
cl = _classify_modal(content, mapping, mapping_meta, valid_codes, error_mode, text_rules)
|
cl = _classify_modal(content, mapping, mapping_meta, valid_codes, error_mode, text_rules)
|
||||||
if cl["blocked_error"]:
|
if cl["blocked_error"]:
|
||||||
# on_unmapped_error=True: respinge fara enqueue (cod necunoscut/nemapat).
|
# on_unmapped_error=True: respinge fara enqueue (cod necunoscut/nemapat).
|
||||||
results.append(_rezultat_respins(None, cl, rar_env=env))
|
results.append(_rezultat_respins(None, cl, rar_env=env))
|
||||||
continue
|
continue
|
||||||
# PRD 5.19 US-002: `held` = snapshot comutator cont (chokepoint held_for_account).
|
# `held` = snapshot comutator cont (chokepoint held_for_account).
|
||||||
cur = conn.execute(
|
cur = conn.execute(
|
||||||
"INSERT INTO submissions (idempotency_key, account_id, status, payload_json, rar_error, rar_creds_enc, rar_env, held) "
|
"INSERT INTO submissions (idempotency_key, account_id, status, payload_json, rar_error, rar_creds_enc, rar_env, held) "
|
||||||
"VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
"VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||||
@@ -401,11 +399,11 @@ def valideaza_prezentari(
|
|||||||
text_rules = load_text_rules(conn, acct)
|
text_rules = load_text_rules(conn, acct)
|
||||||
error_mode = _effective_on_unmapped_error(conn, acct, req.on_unmapped_error)
|
error_mode = _effective_on_unmapped_error(conn, acct, req.on_unmapped_error)
|
||||||
|
|
||||||
# US-005 (DX F5): rezolva env identic ca trimiterea reala si ecou-ieste in raspuns.
|
# Rezolva env identic ca trimiterea reala si ecou-ieste in raspuns.
|
||||||
try:
|
try:
|
||||||
env = rezolva_rar_env(conn, acct, req.rar_env)
|
env = rezolva_rar_env(conn, acct, req.rar_env)
|
||||||
except MediuIndisponibil as e:
|
except MediuIndisponibil as e:
|
||||||
# US-012: audit blocare mediu indisponibil pe dry-run (tip='rar_env_blocat').
|
# Audit blocare mediu indisponibil pe dry-run (tip='rar_env_blocat').
|
||||||
log_event(
|
log_event(
|
||||||
"rar_env_blocat",
|
"rar_env_blocat",
|
||||||
nivel="WARNING",
|
nivel="WARNING",
|
||||||
@@ -464,7 +462,7 @@ def list_prezentari(
|
|||||||
scope_sql, scope_params = account_scope_clause(account_id)
|
scope_sql, scope_params = account_scope_clause(account_id)
|
||||||
# payload_json e plaintext (vezi submissions.payload_json); il citim doar ca
|
# payload_json e plaintext (vezi submissions.payload_json); il citim doar ca
|
||||||
# sa derivam campurile afisabile prin helper-ul partajat, nu il expunem.
|
# sa derivam campurile afisabile prin helper-ul partajat, nu il expunem.
|
||||||
# rar_env inclus (US-005): badge mediu in lista.
|
# rar_env inclus: badge mediu in lista.
|
||||||
cols = (
|
cols = (
|
||||||
"id, status, id_prezentare, rar_status_code, retry_count, "
|
"id, status, id_prezentare, rar_status_code, retry_count, "
|
||||||
"created_at, updated_at, payload_json, rar_env, held"
|
"created_at, updated_at, payload_json, rar_env, held"
|
||||||
@@ -502,9 +500,9 @@ _PREZENTARE_FIELDS = frozenset({
|
|||||||
# erori din catalog (niciodata creds, ex. RAR_CREDS_INVALIDE poarta doar cauza
|
# erori din catalog (niciodata creds, ex. RAR_CREDS_INVALIDE poarta doar cauza
|
||||||
# "credentiale RAR invalide", fara parola). Face recovery-ul observabil prin API.
|
# "credentiale RAR invalide", fara parola). Face recovery-ul observabil prin API.
|
||||||
"rar_error",
|
"rar_error",
|
||||||
# US-005: mediul RAR tinta (Test/Productie) — necesar pentru badge + ecou API.
|
# Mediul RAR tinta (Test/Productie) — necesar pentru badge + ecou API.
|
||||||
"rar_env",
|
"rar_env",
|
||||||
# PRD 5.19 US-010: flag `held` (tinut manual) — onestitate GET (dev vede ca NU a plecat).
|
# Flag `held` (tinut manual) — onestitate GET (dev vede ca NU a plecat).
|
||||||
"held",
|
"held",
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -589,7 +587,7 @@ def trimite_acum_prezentare(
|
|||||||
submission_id: int,
|
submission_id: int,
|
||||||
account_id: int = Depends(resolve_account_id),
|
account_id: int = Depends(resolve_account_id),
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""Elibereaza manual un rand tinut (PRD 5.19 US-010, paritate API cu /repune).
|
"""Elibereaza manual un rand tinut (paritate API cu /repune).
|
||||||
|
|
||||||
Scoped strict pe contul cheii API (account_id din sesiune/cheie, NICIODATA din body).
|
Scoped strict pe contul cheii API (account_id din sesiune/cheie, NICIODATA din body).
|
||||||
404-before-leak pe id strain/inexistent. `held: 1 -> 0` DOAR daca randul e
|
404-before-leak pe id strain/inexistent. `held: 1 -> 0` DOAR daca randul e
|
||||||
@@ -639,7 +637,7 @@ AUDIT_COLUMNS = [
|
|||||||
"odometru_final",
|
"odometru_final",
|
||||||
"prestatii",
|
"prestatii",
|
||||||
"rar_status_code",
|
"rar_status_code",
|
||||||
# US-010 (PRD 5.20): mediul RAR tinta per trimitere.
|
# Mediul RAR tinta per trimitere.
|
||||||
"rar_env",
|
"rar_env",
|
||||||
"created_at",
|
"created_at",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
@@ -694,7 +692,7 @@ def _audit_rows(conn, date_from: str | None, date_to: str | None, status: str, a
|
|||||||
"odometru_final": p.get("odometru_final") or "",
|
"odometru_final": p.get("odometru_final") or "",
|
||||||
"prestatii": codes,
|
"prestatii": codes,
|
||||||
"rar_status_code": r["rar_status_code"] or "",
|
"rar_status_code": r["rar_status_code"] or "",
|
||||||
# US-010 (PRD 5.20): mediul RAR tinta — coloana audit.
|
# Mediul RAR tinta — coloana audit.
|
||||||
"rar_env": r["rar_env"] or "",
|
"rar_env": r["rar_env"] or "",
|
||||||
"created_at": r["created_at"],
|
"created_at": r["created_at"],
|
||||||
"updated_at": r["updated_at"],
|
"updated_at": r["updated_at"],
|
||||||
@@ -826,7 +824,7 @@ def set_rar_creds(
|
|||||||
req: RarCredsIn,
|
req: RarCredsIn,
|
||||||
account_id: int = Depends(resolve_account_id),
|
account_id: int = Depends(resolve_account_id),
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""Seteaza creds RAR durabile per-cont, in slotul per-mediu (US-013, env-aware).
|
"""Seteaza creds RAR durabile per-cont, in slotul per-mediu (env-aware).
|
||||||
|
|
||||||
Slotul tinta: `req.rar_target` ('test'/'prod') sau ancora globala (AUTOPASS_RAR_ENV).
|
Slotul tinta: `req.rar_target` ('test'/'prod') sau ancora globala (AUTOPASS_RAR_ENV).
|
||||||
Activeaza mediul selectat (`rar_{env}_enabled=1`). Worker-ul le foloseste ca
|
Activeaza mediul selectat (`rar_{env}_enabled=1`). Worker-ul le foloseste ca
|
||||||
@@ -855,7 +853,7 @@ def delete_rar_creds(
|
|||||||
account_id: int = Depends(resolve_account_id),
|
account_id: int = Depends(resolve_account_id),
|
||||||
env: str | None = None,
|
env: str | None = None,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""Sterge creds RAR durabile per-cont (revenire la modelul efemer Treapta 1).
|
"""Sterge creds RAR durabile per-cont (revenire la modelul efemer per submission).
|
||||||
|
|
||||||
`env` (query param): 'test' | 'prod' -> sterge DOAR slotul acelui mediu + dezactiveaza-l.
|
`env` (query param): 'test' | 'prod' -> sterge DOAR slotul acelui mediu + dezactiveaza-l.
|
||||||
Absent -> sterge AMBELE sloturi (revenire completa). Schimbare aditiva (back-compat).
|
Absent -> sterge AMBELE sloturi (revenire completa). Schimbare aditiva (back-compat).
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ def resolve_account_id(
|
|||||||
def require_api_access(
|
def require_api_access(
|
||||||
account_id: int = Depends(resolve_account_id),
|
account_id: int = Depends(resolve_account_id),
|
||||||
) -> int:
|
) -> int:
|
||||||
"""Dependency FastAPI (T4, PRD 5.17): verifica ca tier-ul efectiv permite accesul la API.
|
"""Dependency FastAPI: verifica ca tier-ul efectiv permite accesul la API.
|
||||||
|
|
||||||
Reguli:
|
Reguli:
|
||||||
- enforce_plans=False (kill-switch): sare verificarea.
|
- enforce_plans=False (kill-switch): sare verificarea.
|
||||||
|
|||||||
@@ -35,10 +35,10 @@ class Settings(BaseSettings):
|
|||||||
# Retentie randuri blocate (error/needs_data/needs_mapping). Mai scurt decat 90z
|
# Retentie randuri blocate (error/needs_data/needs_mapping). Mai scurt decat 90z
|
||||||
# ale `sent` — un blocat n-are valoare de audit.
|
# ale `sent` — un blocat n-are valoare de audit.
|
||||||
blocked_retention_days: int = 30
|
blocked_retention_days: int = 30
|
||||||
# Prag zile (PRD 5.19 US-007): randurile tinute manual (`queued AND held=1`) mai vechi de
|
# Prag zile: randurile tinute manual (`queued AND held=1`) mai vechi de
|
||||||
# atat declanseaza bannerul de avertizare L.142 (declararea are termen legal).
|
# atat declanseaza bannerul de avertizare L.142 (declararea are termen legal).
|
||||||
held_warn_days: int = 7
|
held_warn_days: int = 7
|
||||||
# Retentie randuri tinute manual (PRD 5.19 US-008): un `queued AND held=1` mai vechi de atat
|
# Retentie randuri tinute manual: un `queued AND held=1` mai vechi de atat
|
||||||
# expira la `error` (`TINUT_EXPIRAT`) + purge_after direct. Aliniat retentiei `sent` (90z).
|
# expira la `error` (`TINUT_EXPIRAT`) + purge_after direct. Aliniat retentiei `sent` (90z).
|
||||||
held_retention_days: int = 90
|
held_retention_days: int = 90
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ class Settings(BaseSettings):
|
|||||||
# False (dev): cookie fara Secure, functioneaza pe HTTP.
|
# False (dev): cookie fara Secure, functioneaza pe HTTP.
|
||||||
session_https_only: bool = False
|
session_https_only: bool = False
|
||||||
|
|
||||||
# --- Contact suport (US-001, PRD 5.12) ---
|
# --- Contact suport ---
|
||||||
# Email/canal de contact afisat in mesaje catre utilizatori (ex. CUI duplicat la signup).
|
# Email/canal de contact afisat in mesaje catre utilizatori (ex. CUI duplicat la signup).
|
||||||
# Nesetat -> fallback la formularea generica fara canal concret.
|
# Nesetat -> fallback la formularea generica fara canal concret.
|
||||||
support_email: str | None = None
|
support_email: str | None = None
|
||||||
@@ -115,14 +115,13 @@ class Settings(BaseSettings):
|
|||||||
worker_retry_max_s: int = 300
|
worker_retry_max_s: int = 300
|
||||||
worker_max_retries: int = 8 # peste atat -> error + banner
|
worker_max_retries: int = 8 # peste atat -> error + banner
|
||||||
|
|
||||||
# --- Planuri de cont (PRD 5.17) ---
|
# --- Planuri de cont ---
|
||||||
# Enforcement DUR al limitelor de plan (volum + acces API). True (implicit) = activ.
|
# Enforcement DUR al limitelor de plan (volum + acces API). True (implicit) = activ.
|
||||||
# False = kill-switch de operare: sare toate gate-urile de plan (util pentru debugging
|
# False = kill-switch de operare: sare toate gate-urile de plan (util pentru debugging
|
||||||
# sau rollback rapid fara revert de cod). Enforcement DUR e activ implicit de la deploy
|
# sau rollback rapid fara revert de cod). Nu exista conturi legacy, produs in teste.
|
||||||
# (decizie user 2026-06-28, decizia #22 autoplan): nu exista conturi legacy, produs in TESTE.
|
|
||||||
enforce_plans: bool = True
|
enforce_plans: bool = True
|
||||||
|
|
||||||
# --- Embeddings (sugestie mapare, Stratul 2 PRD 5.14) ---
|
# --- Embeddings (sugestie mapare) ---
|
||||||
# ACTIVAT implicit: editorul de mapari ofera sugestii semantice (model fastembed/ONNX).
|
# ACTIVAT implicit: editorul de mapari ofera sugestii semantice (model fastembed/ONNX).
|
||||||
# Cost: prima folosire lazy-load-eaza modelul (~230MB pe disc) sincron in thread-ul de
|
# Cost: prima folosire lazy-load-eaza modelul (~230MB pe disc) sincron in thread-ul de
|
||||||
# cerere -> prima cerere /mapari poate dura 30-120s pana modelul intra in memorie; cererile
|
# cerere -> prima cerere /mapari poate dura 30-120s pana modelul intra in memorie; cererile
|
||||||
@@ -131,7 +130,7 @@ class Settings(BaseSettings):
|
|||||||
# /mapari instant la prima cerere sau suita de teste rapida (cade pe GOLD/SILVER+fuzzy).
|
# /mapari instant la prima cerere sau suita de teste rapida (cade pe GOLD/SILVER+fuzzy).
|
||||||
embeddings_enabled: bool = True
|
embeddings_enabled: bool = True
|
||||||
|
|
||||||
# --- Seed corpus operatii etichetate (SILVER, PRD 5.18 US-004) ---
|
# --- Seed corpus operatii etichetate (SILVER) ---
|
||||||
# ACTIVAT implicit: la init_db, populeaza mapping_suggestions din artefactul comis
|
# ACTIVAT implicit: la init_db, populeaza mapping_suggestions din artefactul comis
|
||||||
# `app/data/operatii-etichetate.json` (INSERT OR IGNORE). Asa SILVER nu mai e gol in
|
# `app/data/operatii-etichetate.json` (INSERT OR IGNORE). Asa SILVER nu mai e gol in
|
||||||
# productie -> sugestii exact-match + corpus k-NN reale. SUGGESTION-ONLY.
|
# productie -> sugestii exact-match + corpus k-NN reale. SUGGESTION-ONLY.
|
||||||
|
|||||||
42
app/db.py
42
app/db.py
@@ -37,7 +37,7 @@ def init_db() -> None:
|
|||||||
from .mapping import seed_nomenclator_if_empty
|
from .mapping import seed_nomenclator_if_empty
|
||||||
|
|
||||||
seed_nomenclator_if_empty(conn)
|
seed_nomenclator_if_empty(conn)
|
||||||
# Seed corpus operatii etichetate -> mapping_suggestions (SILVER, PRD 5.18 US-004).
|
# Seed corpus operatii etichetate -> mapping_suggestions (SILVER).
|
||||||
# Gated: OFF in teste (conftest), ON in productie. INSERT OR IGNORE -> idempotent.
|
# Gated: OFF in teste (conftest), ON in productie. INSERT OR IGNORE -> idempotent.
|
||||||
# DOAR daca mapping_suggestions e gol: seedul are ~17k randuri; re-rularea lui pe
|
# DOAR daca mapping_suggestions e gol: seedul are ~17k randuri; re-rularea lui pe
|
||||||
# FIECARE boot (API + worker concurent) tinea write-lock-ul indelung -> al doilea
|
# FIECARE boot (API + worker concurent) tinea write-lock-ul indelung -> al doilea
|
||||||
@@ -72,9 +72,9 @@ def _migrate(conn: sqlite3.Connection) -> None:
|
|||||||
if "row_index" not in sub_cols:
|
if "row_index" not in sub_cols:
|
||||||
conn.execute("ALTER TABLE submissions ADD COLUMN row_index INTEGER")
|
conn.execute("ALTER TABLE submissions ADD COLUMN row_index INTEGER")
|
||||||
if "rar_env" not in sub_cols:
|
if "rar_env" not in sub_cols:
|
||||||
# PRD 5.20 US-001. Mediul RAR tinta pe submission. Pe DB existent NU lasam
|
# Mediul RAR tinta pe submission. Pe DB existent NU lasam
|
||||||
# randurile pe DEFAULT 'test': un rand prod pre-migrare etichetat 'test' ar fi
|
# randurile pe DEFAULT 'test': un rand prod pre-migrare etichetat 'test' ar fi
|
||||||
# reconciliat de worker (US-006) contra endpoint TEST -> no-match -> re-send prod
|
# reconciliat de worker contra endpoint TEST -> no-match -> re-send prod
|
||||||
# = DUPLICAT REAL IREVERSIBIL. Backfill din AUTOPASS_RAR_ENV global (ancora de
|
# = DUPLICAT REAL IREVERSIBIL. Backfill din AUTOPASS_RAR_ENV global (ancora de
|
||||||
# migrare) + recompute idempotency_key env-aware. Ruleaza O SINGURA DATA (in
|
# migrare) + recompute idempotency_key env-aware. Ruleaza O SINGURA DATA (in
|
||||||
# blocul de adaugare a coloanei); pe DB fresh coloana vine din schema.sql (fara rows).
|
# blocul de adaugare a coloanei); pe DB fresh coloana vine din schema.sql (fara rows).
|
||||||
@@ -84,7 +84,7 @@ def _migrate(conn: sqlite3.Connection) -> None:
|
|||||||
)
|
)
|
||||||
_backfill_submissions_rar_env(conn)
|
_backfill_submissions_rar_env(conn)
|
||||||
if "held" not in sub_cols:
|
if "held" not in sub_cols:
|
||||||
# PRD 5.19 US-001: flag rand tinut manual. Default 0 (randurile pre-migrare nu erau
|
# Flag rand tinut manual. Default 0 (randurile pre-migrare nu erau
|
||||||
# tinute — pastreaza comportamentul; comutatorul de cont e OFF/held la ingestie NOUA).
|
# tinute — pastreaza comportamentul; comutatorul de cont e OFF/held la ingestie NOUA).
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"ALTER TABLE submissions ADD COLUMN held INTEGER NOT NULL DEFAULT 0 "
|
"ALTER TABLE submissions ADD COLUMN held INTEGER NOT NULL DEFAULT 0 "
|
||||||
@@ -93,11 +93,11 @@ def _migrate(conn: sqlite3.Connection) -> None:
|
|||||||
|
|
||||||
# Coloane accounts
|
# Coloane accounts
|
||||||
acc_cols = {r["name"] for r in conn.execute("PRAGMA table_info(accounts)").fetchall()}
|
acc_cols = {r["name"] for r in conn.execute("PRAGMA table_info(accounts)").fetchall()}
|
||||||
# AUTO-FIX 6a ELIMINAT (US-013): NU mai adaugam accounts.rar_creds_enc — coloana e dropata.
|
# NU mai adaugam accounts.rar_creds_enc — coloana e dropata.
|
||||||
# _migrate_accounts_medii gestioneaza absenta coloana (guard la ~219: if "rar_creds_enc" not in acc_cols: return).
|
# _migrate_accounts_medii gestioneaza absenta coloana (guard: if "rar_creds_enc" not in acc_cols: return).
|
||||||
# Medii RAR per cont (PRD 5.20 US-001): activare + slot creds + default, per mediu.
|
# Medii RAR per cont: activare + slot creds + default, per mediu.
|
||||||
_migrate_accounts_medii(conn, acc_cols)
|
_migrate_accounts_medii(conn, acc_cols)
|
||||||
# US-013: DROP coloana legacy accounts.rar_creds_enc dupa backfill complet.
|
# DROP coloana legacy accounts.rar_creds_enc dupa backfill complet.
|
||||||
_drop_legacy_accounts_rar_creds(conn, acc_cols)
|
_drop_legacy_accounts_rar_creds(conn, acc_cols)
|
||||||
if "active" not in acc_cols:
|
if "active" not in acc_cols:
|
||||||
# Conturi existente raman active (default 1).
|
# Conturi existente raman active (default 1).
|
||||||
@@ -121,7 +121,7 @@ def _migrate(conn: sqlite3.Connection) -> None:
|
|||||||
"CHECK (on_unmapped_error_default IN (0, 1))"
|
"CHECK (on_unmapped_error_default IN (0, 1))"
|
||||||
)
|
)
|
||||||
if "auto_send_enabled" not in acc_cols:
|
if "auto_send_enabled" not in acc_cols:
|
||||||
# PRD 5.19 US-001: comutator Auto per cont. Default 0 (Auto OFF) — conturile existente
|
# Comutator Auto per cont. Default 0 (Auto OFF) — conturile existente
|
||||||
# trec pe „tine randurile" la ingestie noua pana cand operatorul porneste Auto explicit.
|
# trec pe „tine randurile" la ingestie noua pana cand operatorul porneste Auto explicit.
|
||||||
# Contul id=1 ramane pe default (0), fara tratament special.
|
# Contul id=1 ramane pe default (0), fara tratament special.
|
||||||
conn.execute(
|
conn.execute(
|
||||||
@@ -129,10 +129,10 @@ def _migrate(conn: sqlite3.Connection) -> None:
|
|||||||
"CHECK (auto_send_enabled IN (0, 1))"
|
"CHECK (auto_send_enabled IN (0, 1))"
|
||||||
)
|
)
|
||||||
if "email" not in acc_cols:
|
if "email" not in acc_cols:
|
||||||
# Email canonic de contact al firmei (US-001, PRD 5.12). Nullable pt. conturi legacy.
|
# Email canonic de contact al firmei. Nullable pt. conturi legacy.
|
||||||
conn.execute("ALTER TABLE accounts ADD COLUMN email TEXT")
|
conn.execute("ALTER TABLE accounts ADD COLUMN email TEXT")
|
||||||
if "tier" not in acc_cols:
|
if "tier" not in acc_cols:
|
||||||
# Plan de cont (US-001, PRD 5.17). Legacy -> 'free' fara trial (enforcement DUR la deploy).
|
# Plan de cont. Legacy -> 'free' fara trial (enforcement DUR la deploy).
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"ALTER TABLE accounts ADD COLUMN tier TEXT NOT NULL DEFAULT 'free' "
|
"ALTER TABLE accounts ADD COLUMN tier TEXT NOT NULL DEFAULT 'free' "
|
||||||
"CHECK (tier IN ('free','standard','pro','premium'))"
|
"CHECK (tier IN ('free','standard','pro','premium'))"
|
||||||
@@ -173,7 +173,7 @@ def _migrate(conn: sqlite3.Connection) -> None:
|
|||||||
if "override_json" not in irows_cols:
|
if "override_json" not in irows_cols:
|
||||||
conn.execute("ALTER TABLE import_rows ADD COLUMN override_json TEXT")
|
conn.execute("ALTER TABLE import_rows ADD COLUMN override_json TEXT")
|
||||||
if "reviewed" not in irows_cols:
|
if "reviewed" not in irows_cols:
|
||||||
# Marcaj confirmare umana (US-007, PRD 5.12). NU intra in payload/idempotenta.
|
# Marcaj confirmare umana. NU intra in payload/idempotenta.
|
||||||
# NOT NULL DEFAULT 0: valoare clara (0=neconfirmat), fara ambiguitate NULL vs 0.
|
# NOT NULL DEFAULT 0: valoare clara (0=neconfirmat), fara ambiguitate NULL vs 0.
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"ALTER TABLE import_rows ADD COLUMN reviewed INTEGER NOT NULL DEFAULT 0"
|
"ALTER TABLE import_rows ADD COLUMN reviewed INTEGER NOT NULL DEFAULT 0"
|
||||||
@@ -194,7 +194,7 @@ def _migrate(conn: sqlite3.Connection) -> None:
|
|||||||
"ON submissions(account_id, status)"
|
"ON submissions(account_id, status)"
|
||||||
)
|
)
|
||||||
if "idx_submissions_held" not in existing_idx:
|
if "idx_submissions_held" not in existing_idx:
|
||||||
# PRD 5.19 US-001 (Eng MEDIUM): CREATE TABLE IF NOT EXISTS nu se declanseaza pe DB
|
# CREATE TABLE IF NOT EXISTS nu se declanseaza pe DB
|
||||||
# existent -> indexul partial trebuie creat si aici, nu doar in schema.sql.
|
# existent -> indexul partial trebuie creat si aici, nu doar in schema.sql.
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"CREATE INDEX IF NOT EXISTS idx_submissions_held ON submissions(held) WHERE held=1"
|
"CREATE INDEX IF NOT EXISTS idx_submissions_held ON submissions(held) WHERE held=1"
|
||||||
@@ -202,7 +202,7 @@ def _migrate(conn: sqlite3.Connection) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def _migrate_accounts_medii(conn: sqlite3.Connection, acc_cols: set[str]) -> None:
|
def _migrate_accounts_medii(conn: sqlite3.Connection, acc_cols: set[str]) -> None:
|
||||||
"""PRD 5.20 US-001: coloane medii RAR per cont + backfill din ancora globala.
|
"""Coloane medii RAR per cont + backfill din ancora globala.
|
||||||
|
|
||||||
Adauga (idempotent): rar_test_enabled/rar_prod_enabled (bife activare),
|
Adauga (idempotent): rar_test_enabled/rar_prod_enabled (bife activare),
|
||||||
rar_creds_test_enc/rar_creds_prod_enc (sloturi creds), rar_env_default.
|
rar_creds_test_enc/rar_creds_prod_enc (sloturi creds), rar_env_default.
|
||||||
@@ -212,7 +212,7 @@ def _migrate_accounts_medii(conn: sqlite3.Connection, acc_cols: set[str]) -> Non
|
|||||||
momentul migrarii (ancora) — il copiem in slotul acelui mediu, activam DOAR acel
|
momentul migrarii (ancora) — il copiem in slotul acelui mediu, activam DOAR acel
|
||||||
mediu (celalalt dezactivat) si fixam default-ul pe el. Conturile fara creds raman
|
mediu (celalalt dezactivat) si fixam default-ul pe el. Conturile fara creds raman
|
||||||
pe default-urile coloanei (prod on / test off). Migrarea NU presupune env-ul; se
|
pe default-urile coloanei (prod on / test off). Migrarea NU presupune env-ul; se
|
||||||
bazeaza pe ancora globala, exact cum opera contul inainte de 5.20.
|
bazeaza pe ancora globala.
|
||||||
"""
|
"""
|
||||||
newly_added = "rar_env_default" not in acc_cols
|
newly_added = "rar_env_default" not in acc_cols
|
||||||
if "rar_test_enabled" not in acc_cols:
|
if "rar_test_enabled" not in acc_cols:
|
||||||
@@ -252,12 +252,12 @@ def _migrate_accounts_medii(conn: sqlite3.Connection, acc_cols: set[str]) -> Non
|
|||||||
|
|
||||||
|
|
||||||
def _drop_legacy_accounts_rar_creds(conn: sqlite3.Connection, acc_cols: set[str]) -> None:
|
def _drop_legacy_accounts_rar_creds(conn: sqlite3.Connection, acc_cols: set[str]) -> None:
|
||||||
"""PRD 5.20 US-013: DROP coloana legacy `accounts.rar_creds_enc` dupa backfill complet.
|
"""DROP coloana legacy `accounts.rar_creds_enc` dupa backfill complet.
|
||||||
|
|
||||||
Idempotent si sigur la fiecare boot (garda one-way: coloana absenta = nimic de facut).
|
Idempotent si sigur la fiecare boot (garda one-way: coloana absenta = nimic de facut).
|
||||||
La eroare LOGHEAZA si lasa coloana pe loc (fail-safe — nu crapa boot-ul ambelor procese).
|
La eroare LOGHEAZA si lasa coloana pe loc (fail-safe — nu crapa boot-ul ambelor procese).
|
||||||
Structura separata pentru testabilitate: `_garda_si_drop(conn)` expune pasul de
|
Structura separata pentru testabilitate: `_garda_si_drop(conn)` expune pasul de
|
||||||
assert+backup+DROP izolat (fara re-backfill 6c), apelabil direct din teste.
|
assert+backup+DROP izolat (fara re-backfill), apelabil direct din teste.
|
||||||
"""
|
"""
|
||||||
if "rar_creds_enc" not in acc_cols:
|
if "rar_creds_enc" not in acc_cols:
|
||||||
return # garda one-way: coloana deja dropata sau DB fresh
|
return # garda one-way: coloana deja dropata sau DB fresh
|
||||||
@@ -272,10 +272,10 @@ def _drop_legacy_accounts_rar_creds(conn: sqlite3.Connection, acc_cols: set[str]
|
|||||||
|
|
||||||
|
|
||||||
def _drop_legacy_rar_creds_impl(conn: sqlite3.Connection) -> None:
|
def _drop_legacy_rar_creds_impl(conn: sqlite3.Connection) -> None:
|
||||||
"""Re-backfill (AUTO-FIX 6c) + delegate la _garda_si_drop.
|
"""Re-backfill + delegate la _garda_si_drop.
|
||||||
|
|
||||||
Re-backfill-ul 6c acopera creds setate via POST /v1/conturi/rar-creds intre US-001
|
Re-backfill-ul acopera creds setate via POST /v1/conturi/rar-creds intre adaugarea
|
||||||
si US-013 (pot fi DOAR in coloana veche). Ancora globala: AUTOPASS_RAR_ENV.
|
coloanei noi si drop-ul celei vechi (pot fi DOAR in coloana veche). Ancora globala: AUTOPASS_RAR_ENV.
|
||||||
"""
|
"""
|
||||||
if sqlite3.sqlite_version_info < (3, 35, 0):
|
if sqlite3.sqlite_version_info < (3, 35, 0):
|
||||||
print(
|
print(
|
||||||
@@ -344,7 +344,7 @@ def _garda_si_drop(conn: sqlite3.Connection) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def _backfill_submissions_rar_env(conn: sqlite3.Connection) -> None:
|
def _backfill_submissions_rar_env(conn: sqlite3.Connection) -> None:
|
||||||
"""PRD 5.20 US-001 (AUTO-FIX G + E4/3): backfill rar_env + recompute idempotency_key.
|
"""Backfill rar_env + recompute idempotency_key.
|
||||||
|
|
||||||
Ruleaza O SINGURA DATA, imediat dupa ce coloana `submissions.rar_env` a fost adaugata
|
Ruleaza O SINGURA DATA, imediat dupa ce coloana `submissions.rar_env` a fost adaugata
|
||||||
pe un DB existent. Toate randurile pre-migrare au fost trimise (sau urmeaza) catre
|
pe un DB existent. Toate randurile pre-migrare au fost trimise (sau urmeaza) catre
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
"""Modul embedding in-proces pentru sugestie cod RAR -- L14-S4.
|
"""Modul embedding in-proces pentru sugestie cod RAR.
|
||||||
|
|
||||||
Design (PRD 5.14, Decision #16/#16b):
|
Design:
|
||||||
- Model multilingv via fastembed/ONNX (~230MB pe disc, quantizat, fara torch)
|
- Model multilingv via fastembed/ONNX (~230MB pe disc, quantizat, fara torch)
|
||||||
- Lazy load la prima folosire, NU la import si NU pe /healthz
|
- Lazy load la prima folosire, NU la import si NU pe /healthz
|
||||||
- Worker NU incarca modelul (API-only)
|
- Worker NU incarca modelul (API-only)
|
||||||
- Degradare gratioasa: daca modelul nu se incarca -> is_available()=False,
|
- Degradare gratioasa: daca modelul nu se incarca -> is_available()=False,
|
||||||
suggest_nearest() -> [] fara exceptie, ingestia NU e blocata
|
suggest_nearest() -> [] fara exceptie, ingestia NU e blocata
|
||||||
- Embeddings = DOAR sugestie (nu intra in lantul de enqueue/resolve_prestatii)
|
- Embeddings = DOAR sugestie (nu intra in lantul de enqueue/resolve_prestatii)
|
||||||
- NU apelat din resolve_prestatii/load_mapping (wiring vine in L14-S6 DUPA 5.15)
|
- NU apelat din resolve_prestatii/load_mapping
|
||||||
|
|
||||||
API public (nivel modul):
|
API public (nivel modul):
|
||||||
index_corpus(items) -> None
|
index_corpus(items) -> None
|
||||||
@@ -28,7 +28,7 @@ log = logging.getLogger(__name__)
|
|||||||
|
|
||||||
# Modelul ales: paraphrase-multilingual-MiniLM-L12-v2
|
# Modelul ales: paraphrase-multilingual-MiniLM-L12-v2
|
||||||
# ~230MB pe disc (ONNX quantizat), 384 dim, multilingv (ro/en/etc.), suportat de
|
# ~230MB pe disc (ONNX quantizat), 384 dim, multilingv (ro/en/etc.), suportat de
|
||||||
# fastembed, fara torch. (Estimarea initiala din PRD de ~50MB a fost gresita.)
|
# fastembed, fara torch.
|
||||||
FASTEMBED_MODEL = "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2"
|
FASTEMBED_MODEL = "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2"
|
||||||
|
|
||||||
|
|
||||||
@@ -137,7 +137,7 @@ class EmbeddingEngine:
|
|||||||
) -> list[dict]:
|
) -> list[dict]:
|
||||||
"""Returneaza top_k vecini cosine [{cod, is_nul, similaritate}].
|
"""Returneaza top_k vecini cosine [{cod, is_nul, similaritate}].
|
||||||
|
|
||||||
`is_nul` (PRD 5.18 US-005): cand corpusul include exemple NUL (non-operatii),
|
`is_nul`: cand corpusul include exemple NUL (non-operatii),
|
||||||
un vecin NUL = semnal de SUPRESIE, nu cod. Default False pe corpusuri vechi
|
un vecin NUL = semnal de SUPRESIE, nu cod. Default False pe corpusuri vechi
|
||||||
fara `is_nul` in itemi. Returneaza [] daca backend-ul lipseste, corpus-ul e gol
|
fara `is_nul` in itemi. Returneaza [] daca backend-ul lipseste, corpus-ul e gol
|
||||||
sau apare orice exceptie (degradare gratioasa -- nu blocheaza ingestia).
|
sau apare orice exceptie (degradare gratioasa -- nu blocheaza ingestia).
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ CATALOG: dict[str, dict[str, str]] = {
|
|||||||
" cererii (request_id) afisat."
|
" cererii (request_id) afisat."
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
# Coduri de plan (PRD 5.17)
|
# Coduri de plan
|
||||||
"PLAN_LIMITA_LUNARA": {
|
"PLAN_LIMITA_LUNARA": {
|
||||||
"problema": "Ai atins limita planului Gratuit (60 prestatii/luna)",
|
"problema": "Ai atins limita planului Gratuit (60 prestatii/luna)",
|
||||||
"fix": (
|
"fix": (
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ def canonicalize_row(raw: dict[str, Any]) -> dict[str, Any]:
|
|||||||
|
|
||||||
|
|
||||||
def build_key(account_id: int | None, canon: dict[str, Any], rar_env: str = "test") -> str:
|
def build_key(account_id: int | None, canon: dict[str, Any], rar_env: str = "test") -> str:
|
||||||
"""SHA-256 partajat canal-API + canal-import, env-aware (PRD 5.20 US-003).
|
"""SHA-256 partajat canal-API + canal-import, env-aware.
|
||||||
|
|
||||||
Aplica account_or_default inainte de hash: None si 1 colapseaza la aceeasi
|
Aplica account_or_default inainte de hash: None si 1 colapseaza la aceeasi
|
||||||
cheie => acelasi rand logic din canale diferite nu se trimite de doua ori.
|
cheie => acelasi rand logic din canale diferite nu se trimite de doua ori.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Parser fisiere xlsx/csv pentru import prezentari (Treapta 2).
|
"""Parser fisiere xlsx/csv pentru import prezentari.
|
||||||
|
|
||||||
Arhitectura 2-treceri:
|
Arhitectura 2-treceri:
|
||||||
Trecerea 1 — read_only=True: dim-check (FileTooLarge) + detectie multi-sheet.
|
Trecerea 1 — read_only=True: dim-check (FileTooLarge) + detectie multi-sheet.
|
||||||
|
|||||||
@@ -187,7 +187,7 @@ def metrics() -> str:
|
|||||||
conn = get_connection()
|
conn = get_connection()
|
||||||
try:
|
try:
|
||||||
rows = conn.execute("SELECT status, COUNT(*) AS n FROM submissions GROUP BY status").fetchall()
|
rows = conn.execute("SELECT status, COUNT(*) AS n FROM submissions GROUP BY status").fetchall()
|
||||||
# Coada tinuta (PRD 5.19 US-007): observabilitate ops pt. default Auto OFF.
|
# Coada tinuta: observabilitate ops pt. default Auto OFF.
|
||||||
# Gauge-uri DERIVATE (zero stare noua): total randuri tinute + varsta celui
|
# Gauge-uri DERIVATE (zero stare noua): total randuri tinute + varsta celui
|
||||||
# mai vechi (din created_at), scoped global. Semnaleaza esecul silentios
|
# mai vechi (din created_at), scoped global. Semnaleaza esecul silentios
|
||||||
# cand prezentari raman nedeclarate tacit (risc L.142).
|
# cand prezentari raman nedeclarate tacit (risc L.142).
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ def normalize_for_match(value: object) -> str:
|
|||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------------- #
|
# --------------------------------------------------------------------------- #
|
||||||
# Pre-filtru determinist non-operatii (NUL) — US-001 PRD 5.18 #
|
# Pre-filtru determinist non-operatii (NUL) #
|
||||||
# --------------------------------------------------------------------------- #
|
# --------------------------------------------------------------------------- #
|
||||||
#
|
#
|
||||||
# Masuratoarea k-NN (memorie test-precizie-knn-embeddings) arata recall NUL doar
|
# Masuratoarea k-NN (memorie test-precizie-knn-embeddings) arata recall NUL doar
|
||||||
@@ -88,7 +88,7 @@ _NUL_CTX_PIESA = re.compile(
|
|||||||
def prefiltru_nul(denumire: object) -> bool:
|
def prefiltru_nul(denumire: object) -> bool:
|
||||||
"""True daca operatia e gunoi evident (non-operatie de service) -> NUL determinist.
|
"""True daca operatia e gunoi evident (non-operatie de service) -> NUL determinist.
|
||||||
|
|
||||||
Ruleaza INAINTE de k-NN/embeddings in `enrich_suggestions` (US-006). Pur, fara DB.
|
Ruleaza INAINTE de k-NN/embeddings in `enrich_suggestions`. Pur, fara DB.
|
||||||
Zero fals-pozitiv pe operatii reale (vezi comentariul de mai sus + tests).
|
Zero fals-pozitiv pe operatii reale (vezi comentariul de mai sus + tests).
|
||||||
"""
|
"""
|
||||||
text = normalize_for_match(denumire)
|
text = normalize_for_match(denumire)
|
||||||
@@ -306,7 +306,7 @@ def resolve_prestatii(
|
|||||||
# Adnotare aditiva: marcheaza ca rezolvat-prin-regula cu pattern-ul
|
# Adnotare aditiva: marcheaza ca rezolvat-prin-regula cu pattern-ul
|
||||||
# sursa. Payload-harmless (RAR citeste doar cod_prestatie).
|
# sursa. Payload-harmless (RAR citeste doar cod_prestatie).
|
||||||
it["cod_sursa"] = f"{COD_SURSA_TEXT_RULE_PREFIX}{pattern_regula or ''}"
|
it["cod_sursa"] = f"{COD_SURSA_TEXT_RULE_PREFIX}{pattern_regula or ''}"
|
||||||
# US-001 (PRD 5.11): regula_fara_autosend nu se mai seteaza;
|
# regula_fara_autosend nu se mai seteaza;
|
||||||
# auto_send nu mai tine randul (has_no_auto_send neutralizat).
|
# auto_send nu mai tine randul (has_no_auto_send neutralizat).
|
||||||
else:
|
else:
|
||||||
it["cod_prestatie"] = None
|
it["cod_prestatie"] = None
|
||||||
@@ -466,7 +466,7 @@ def classify_prezentare(
|
|||||||
status = "needs_data"
|
status = "needs_data"
|
||||||
rar_error = json.dumps(errors, ensure_ascii=False)
|
rar_error = json.dumps(errors, ensure_ascii=False)
|
||||||
else:
|
else:
|
||||||
# US-001 (PRD 5.11): ramura AUTO_SEND_OPRIT eliminata.
|
# Ramura AUTO_SEND_OPRIT eliminata.
|
||||||
# Un cod rezolvat (mapare exacta sau regula text) -> queued direct.
|
# Un cod rezolvat (mapare exacta sau regula text) -> queued direct.
|
||||||
status = "queued"
|
status = "queued"
|
||||||
rar_error = None
|
rar_error = None
|
||||||
@@ -482,7 +482,7 @@ def classify_prezentare(
|
|||||||
|
|
||||||
|
|
||||||
def has_no_auto_send(resolved: list[dict], mapping_meta: dict[str, dict]) -> bool:
|
def has_no_auto_send(resolved: list[dict], mapping_meta: dict[str, dict]) -> bool:
|
||||||
"""Neutralizat dupa US-001 (PRD 5.11): auto_send nu mai tine randuri in needs_mapping.
|
"""Neutralizat: auto_send nu mai tine randuri in needs_mapping.
|
||||||
|
|
||||||
Simbolul este PASTRAT (importat in routes.py si import_router.py); stergerea
|
Simbolul este PASTRAT (importat in routes.py si import_router.py); stergerea
|
||||||
ar produce ImportError la boot. Functia intoarce mereu False — codul rezolvat
|
ar produce ImportError la boot. Functia intoarce mereu False — codul rezolvat
|
||||||
@@ -647,7 +647,7 @@ def _corpus_signature_silver(rows: list) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def ensure_embeddings_corpus(conn, nomenclator: list[dict] | None = None) -> None:
|
def ensure_embeddings_corpus(conn, nomenclator: list[dict] | None = None) -> None:
|
||||||
"""Construieste/actualizeaza corpusul embeddings din corpusul ETICHETAT (PRD 5.18 US-005).
|
"""Construieste/actualizeaza corpusul embeddings din corpusul ETICHETAT.
|
||||||
|
|
||||||
Sursa corpusului = `mapping_suggestions` (SILVER): exemple reale etichetate
|
Sursa corpusului = `mapping_suggestions` (SILVER): exemple reale etichetate
|
||||||
{denumire_normalizata -> cod, is_nul}, NU cele 18 categorii generice din
|
{denumire_normalizata -> cod, is_nul}, NU cele 18 categorii generice din
|
||||||
@@ -660,8 +660,8 @@ def ensure_embeddings_corpus(conn, nomenclator: list[dict] | None = None) -> Non
|
|||||||
|
|
||||||
Cand e activat: indexeaza corpusul o singura data (lazy-load modelul ~230MB la
|
Cand e activat: indexeaza corpusul o singura data (lazy-load modelul ~230MB la
|
||||||
prima chemare), re-indexeaza doar cand semnatura corpusului SILVER s-a schimbat.
|
prima chemare), re-indexeaza doar cand semnatura corpusului SILVER s-a schimbat.
|
||||||
Itemii NUL (is_nul=1, cod NULL) raman in corpus: un vecin NUL e semnal de supresie
|
Itemii NUL (is_nul=1, cod NULL) raman in corpus: un vecin NUL e semnal de supresie.
|
||||||
(US-006). Degradare gratioasa: orice eroare lasa corpusul gol -> enrich cade pe restul.
|
Degradare gratioasa: orice eroare lasa corpusul gol -> enrich cade pe restul.
|
||||||
"""
|
"""
|
||||||
from .config import get_settings
|
from .config import get_settings
|
||||||
if not get_settings().embeddings_enabled:
|
if not get_settings().embeddings_enabled:
|
||||||
@@ -687,7 +687,7 @@ def ensure_embeddings_corpus(conn, nomenclator: list[dict] | None = None) -> Non
|
|||||||
]
|
]
|
||||||
_emb.index_corpus(items, signature=sig)
|
_emb.index_corpus(items, signature=sig)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass # degradare gratioasa (#16b): esecul indexarii nu blocheaza editorul
|
pass # degradare gratioasa: esecul indexarii nu blocheaza editorul
|
||||||
|
|
||||||
|
|
||||||
def enrich_suggestions(
|
def enrich_suggestions(
|
||||||
@@ -698,13 +698,13 @@ def enrich_suggestions(
|
|||||||
) -> dict:
|
) -> dict:
|
||||||
"""Imbogateste sugestiile cu GOLD partajat, SILVER LLM si embeddings NN.
|
"""Imbogateste sugestiile cu GOLD partajat, SILVER LLM si embeddings NN.
|
||||||
|
|
||||||
Precedenta Eng-F2 (pentru sugestie-only, nu auto-send):
|
Precedenta (pentru sugestie-only, nu auto-send):
|
||||||
shared GOLD > SILVER > embeddings
|
shared GOLD > SILVER > embeddings
|
||||||
|
|
||||||
(Account GOLD = operations_mapping propriu = deja rezolvat inainte de needs_mapping;
|
(Account GOLD = operations_mapping propriu = deja rezolvat inainte de needs_mapping;
|
||||||
nu apare in needs_mapping, deci nu e in precedenta de sugestie.)
|
nu apare in needs_mapping, deci nu e in precedenta de sugestie.)
|
||||||
|
|
||||||
Ordine completa (PRD 5.18 US-006):
|
Ordine completa:
|
||||||
pre-filtru NUL determinist -> (daca NUL: fara cod, `surse['nul']=True`)
|
pre-filtru NUL determinist -> (daca NUL: fara cod, `surse['nul']=True`)
|
||||||
altfel GOLD partajat > exact (SILVER) > k-NN embeddings.
|
altfel GOLD partajat > exact (SILVER) > k-NN embeddings.
|
||||||
|
|
||||||
@@ -715,11 +715,11 @@ def enrich_suggestions(
|
|||||||
}
|
}
|
||||||
|
|
||||||
INVARIANTE:
|
INVARIANTE:
|
||||||
- Toate sursele = SUGGESTION-ONLY. NU intra in resolve_prestatii/load_mapping (#13).
|
- Toate sursele = SUGGESTION-ONLY. NU intra in resolve_prestatii/load_mapping.
|
||||||
- Pre-filtru NUL (US-001) ruleaza PRIMUL: gunoiul evident (ITP/plata/discount...) e
|
- Pre-filtru NUL ruleaza PRIMUL: gunoiul evident (ITP/plata/discount...) e
|
||||||
marcat non-operatie INAINTE de k-NN, fara sugestie de cod.
|
marcat non-operatie INAINTE de k-NN, fara sugestie de cod.
|
||||||
- SILVER cu is_nul=1 (non-operatie/gunoi) NU produce sugestie (#4); vecin k-NN NUL idem.
|
- SILVER cu is_nul=1 (non-operatie/gunoi) NU produce sugestie; vecin k-NN NUL idem.
|
||||||
- Degradare gratioasa pe embeddings (#16b): daca motorul nu e disponibil sau arunca,
|
- Degradare gratioasa pe embeddings: daca motorul nu e disponibil sau arunca,
|
||||||
returneaza sugestia disponibila din celelalte surse, fara exceptie.
|
returneaza sugestia disponibila din celelalte surse, fara exceptie.
|
||||||
- Import local shared_store/embeddings: evita ciclu la import-time (shared_store
|
- Import local shared_store/embeddings: evita ciclu la import-time (shared_store
|
||||||
importa normalize_for_match din mapping).
|
importa normalize_for_match din mapping).
|
||||||
@@ -730,7 +730,7 @@ def enrich_suggestions(
|
|||||||
if not denumire:
|
if not denumire:
|
||||||
return {"sugestie_principala": sugestie_principala, "surse": surse}
|
return {"sugestie_principala": sugestie_principala, "surse": surse}
|
||||||
|
|
||||||
# 0. Pre-filtru NUL determinist (US-001) INAINTE de orice k-NN/lookup: non-operatie
|
# 0. Pre-filtru NUL determinist INAINTE de orice k-NN/lookup: non-operatie
|
||||||
# evidenta -> fara cod, scurtcircuit (nu interogheaza embeddings/SILVER pe gunoi).
|
# evidenta -> fara cod, scurtcircuit (nu interogheaza embeddings/SILVER pe gunoi).
|
||||||
if prefiltru_nul(denumire):
|
if prefiltru_nul(denumire):
|
||||||
surse["nul"] = True
|
surse["nul"] = True
|
||||||
@@ -758,7 +758,7 @@ def enrich_suggestions(
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass # degradare gratioasa
|
pass # degradare gratioasa
|
||||||
|
|
||||||
# 3. Embeddings NN (similaritate semantica, degradare gratioasa #16b)
|
# 3. Embeddings NN (similaritate semantica, degradare gratioasa)
|
||||||
if include_embeddings:
|
if include_embeddings:
|
||||||
try:
|
try:
|
||||||
from . import embeddings as _emb
|
from . import embeddings as _emb
|
||||||
@@ -768,7 +768,7 @@ def enrich_suggestions(
|
|||||||
# ensure_embeddings_corpus (gated pe AUTOPASS_EMBEDDINGS_ENABLED); cand
|
# ensure_embeddings_corpus (gated pe AUTOPASS_EMBEDDINGS_ENABLED); cand
|
||||||
# flagul e off, has_corpus() ramane False si calea e un no-op real.
|
# flagul e off, has_corpus() ramane False si calea e un no-op real.
|
||||||
if _emb.has_corpus():
|
if _emb.has_corpus():
|
||||||
# F1 (US-005): corpusul k-NN e text NORMALIZAT (denumire_normalizata),
|
# Corpusul k-NN e text NORMALIZAT (denumire_normalizata),
|
||||||
# deci query-ul TREBUIE normalizat la fel — altfel cosine degradeaza si
|
# deci query-ul TREBUIE normalizat la fel — altfel cosine degradeaza si
|
||||||
# nu mai e configul sub care s-a masurat 94.3%.
|
# nu mai e configul sub care s-a masurat 94.3%.
|
||||||
nn = _emb.suggest_nearest(normalize_for_match(denumire), top_k=1)
|
nn = _emb.suggest_nearest(normalize_for_match(denumire), top_k=1)
|
||||||
@@ -776,7 +776,7 @@ def enrich_suggestions(
|
|||||||
# Evita recomandari irelevante cand corpus-ul e mic/partial.
|
# Evita recomandari irelevante cand corpus-ul e mic/partial.
|
||||||
if nn and nn[0].get("similaritate", 0) >= EMB_MIN_SIMILARITATE:
|
if nn and nn[0].get("similaritate", 0) >= EMB_MIN_SIMILARITATE:
|
||||||
if nn[0].get("is_nul"):
|
if nn[0].get("is_nul"):
|
||||||
# Vecin NUL (non-operatie) = semnal de SUPRESIE, nu cod (US-006).
|
# Vecin NUL (non-operatie) = semnal de SUPRESIE, nu cod.
|
||||||
surse["nul"] = True
|
surse["nul"] = True
|
||||||
elif nn[0].get("cod"):
|
elif nn[0].get("cod"):
|
||||||
surse["embedding"] = str(nn[0]["cod"])
|
surse["embedding"] = str(nn[0]["cod"])
|
||||||
@@ -880,7 +880,7 @@ def reresolve_account(conn, account_id: int | None, batch_id: int | None = None)
|
|||||||
stats["still_blocked"] += 1
|
stats["still_blocked"] += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# US-001 (PRD 5.11): ramura auto_send eliminata din reresolve.
|
# Ramura auto_send eliminata din reresolve.
|
||||||
# Un cod rezolvat -> queued direct (review_manual ramane 0).
|
# Un cod rezolvat -> queued direct (review_manual ramane 0).
|
||||||
|
|
||||||
errors = validate_prezentare(content)
|
errors = validate_prezentare(content)
|
||||||
@@ -892,10 +892,10 @@ def reresolve_account(conn, account_id: int | None, batch_id: int | None = None)
|
|||||||
)
|
)
|
||||||
stats["needs_data"] += 1
|
stats["needs_data"] += 1
|
||||||
else:
|
else:
|
||||||
# PRD 5.19 US-002: re-snapshot `held` din comutatorul contului la trecerea
|
# Re-snapshot `held` din comutatorul contului la trecerea
|
||||||
# needs_mapping -> queued (chokepoint held_for_account, `acct` deja normalizat
|
# needs_mapping -> queued (chokepoint held_for_account, `acct` deja normalizat
|
||||||
# prin account_or_default la intrarea in reresolve_account). Altfel un rand
|
# prin account_or_default la intrarea in reresolve_account). Altfel un rand
|
||||||
# deblocat din needs_mapping ar pleca automat desi contul e Auto OFF (R2).
|
# deblocat din needs_mapping ar pleca automat desi contul e Auto OFF.
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"UPDATE submissions SET status='queued', payload_json=?, rar_error=NULL, "
|
"UPDATE submissions SET status='queued', payload_json=?, rar_error=NULL, "
|
||||||
"retry_count=0, next_attempt_at=NULL, held=?, updated_at=datetime('now') WHERE id=?",
|
"retry_count=0, next_attempt_at=NULL, held=?, updated_at=datetime('now') WHERE id=?",
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ class SubmissionResult(BaseModel):
|
|||||||
# RE-ACTIVAT (re-clasificat + creds actualizate) la resubmit. `deduped` pastreaza
|
# RE-ACTIVAT (re-clasificat + creds actualizate) la resubmit. `deduped` pastreaza
|
||||||
# semantica actuala (clientii vechi care testeaza `deduped` nu se sparg).
|
# semantica actuala (clientii vechi care testeaza `deduped` nu se sparg).
|
||||||
reactivated: bool = False
|
reactivated: bool = False
|
||||||
# Ecou PUR de coada (PRD 5.19 US-010): True cand randul e `queued AND held=1`
|
# Ecou PUR de coada: True cand randul e `queued AND held=1`
|
||||||
# (tinut manual, cont Auto OFF) -> NU a plecat la RAR, asteapta eliberarea umana.
|
# (tinut manual, cont Auto OFF) -> NU a plecat la RAR, asteapta eliberarea umana.
|
||||||
# NU influenteaza payload/idempotenta; onestitate fata de integratorul API (invariant 5.7).
|
# NU influenteaza payload/idempotenta; onestitate fata de integratorul API (invariant 5.7).
|
||||||
held: bool = False
|
held: bool = False
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Seeder corpus operatii etichetate -> mapping_suggestions (SILVER, PRD 5.18 US-004).
|
"""Seeder corpus operatii etichetate -> mapping_suggestions (SILVER).
|
||||||
|
|
||||||
Artefactul `app/data/operatii-etichetate.json` e produs offline de
|
Artefactul `app/data/operatii-etichetate.json` e produs offline de
|
||||||
`tools/mapare-llm/genereaza_seed.py` (etichetare LM Studio, o singura data) si comis
|
`tools/mapare-llm/genereaza_seed.py` (etichetare LM Studio, o singura data) si comis
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""Constructor payload postPrezentare (T4).
|
"""Constructor payload postPrezentare.
|
||||||
|
|
||||||
Reguli din contract (docs/api-rar-contract.md), confirmate live in T1:
|
Reguli din contract (docs/api-rar-contract.md), confirmate live:
|
||||||
- status mereu "FINALIZATA".
|
- status mereu "FINALIZATA".
|
||||||
- tipPrestatie NU se trimite (server-generated GENERIC).
|
- tipPrestatie NU se trimite (server-generated GENERIC).
|
||||||
- odometruFinal ca string; odometruInitial ca string cand e prezent, altfel null.
|
- odometruFinal ca string; odometruInitial ca string cand e prezent, altfel null.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
"""Extragere payload submission -> campuri afisabile.
|
"""Extragere payload submission -> campuri afisabile.
|
||||||
|
|
||||||
Helper PUR partajat intre canalul web (dashboard Trimiteri) si canalul API
|
Helper PUR partajat intre canalul web (dashboard Trimiteri) si canalul API
|
||||||
(`GET /v1/prezentari`), ca extragerea sa NU diverge intre cele doua (decizie
|
(`GET /v1/prezentari`), ca extragerea sa NU diverge intre cele doua.
|
||||||
eng review, DRY). Fara DB, fara request — primeste `payload_json` (text JSON
|
Fara DB, fara request — primeste `payload_json` (text JSON
|
||||||
plaintext, vezi `submissions.payload_json`) sau un dict deja parsat.
|
plaintext, vezi `submissions.payload_json`) sau un dict deja parsat.
|
||||||
|
|
||||||
Defensiv prin constructie: nu arunca niciodata pe payload malformat — degradeaza
|
Defensiv prin constructie: nu arunca niciodata pe payload malformat — degradeaza
|
||||||
@@ -126,7 +126,7 @@ def prezentare_din_payload(payload: str | dict | None) -> dict[str, str]:
|
|||||||
op_service_denumire = _clean_str(item.get("denumire")) if op_service_cod else ""
|
op_service_denumire = _clean_str(item.get("denumire")) if op_service_cod else ""
|
||||||
|
|
||||||
# obs: text liber observatii (camp RAR, optional). Conventie goala "" (nu EMPTY).
|
# obs: text liber observatii (camp RAR, optional). Conventie goala "" (nu EMPTY).
|
||||||
# US-005 PRD 5.15: obs traieste in payload_json (nu coloana separata).
|
# Traieste in payload_json, nu in coloana separata.
|
||||||
obs = _clean_str(data.get("obs"))
|
obs = _clean_str(data.get("obs"))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
17
app/plans.py
17
app/plans.py
@@ -3,11 +3,11 @@
|
|||||||
Pattern ca app/errors.py: catalog + helperi. Consumat de rutele de ingestie si dashboard.
|
Pattern ca app/errors.py: catalog + helperi. Consumat de rutele de ingestie si dashboard.
|
||||||
Nu importa DB, HTTP, sau orice alt modul intern cu efecte secundare.
|
Nu importa DB, HTTP, sau orice alt modul intern cu efecte secundare.
|
||||||
|
|
||||||
Decizii implementare (PRD 5.17 / autoplan 2026-06-28):
|
Note implementare:
|
||||||
- FREE_MONTHLY_LIMIT: constanta unica (T-CEO-2), tunabila fara arqueologie de cod.
|
- FREE_MONTHLY_LIMIT: constanta unica, tunabila fara arheologie de cod.
|
||||||
- CONSUMED_STATUSES: decizie #20 — prestatie consumata = acceptata in coada.
|
- CONSUMED_STATUSES: prestatie consumata = acceptata in coada.
|
||||||
- effective_tier: `now` injectabil (decizie #2) pentru teste deterministe.
|
- effective_tier: `now` injectabil pentru teste deterministe.
|
||||||
- monthly_usage: pattern E7/5.15 (strftime localtime), `now` injectabil.
|
- monthly_usage: strftime localtime, `now` injectabil.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -16,11 +16,10 @@ import sqlite3
|
|||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
# Limita lunara pentru planul Gratuit.
|
# Limita lunara pentru planul Gratuit.
|
||||||
# Decizie user T-CEO-2 (2026-06-28): o singura constanta, referita din PLANS.
|
# O singura constanta, referita din PLANS. Tunabila fara a modifica logica de enforcement.
|
||||||
# Tunabila fara a modifica logica de enforcement.
|
|
||||||
FREE_MONTHLY_LIMIT: int = 60
|
FREE_MONTHLY_LIMIT: int = 60
|
||||||
|
|
||||||
# Statusurile care consuma din cota lunara (decizie #20, 2026-06-28).
|
# Statusurile care consuma din cota lunara.
|
||||||
# Prestatie consumata = acceptata in coada (queued/sending/sent), nu cele respinse/blocate.
|
# Prestatie consumata = acceptata in coada (queued/sending/sent), nu cele respinse/blocate.
|
||||||
# Rationale: limita e pe ce trimitem la RAR, nu pe incercari esuate sau blocate.
|
# Rationale: limita e pe ce trimitem la RAR, nu pe incercari esuate sau blocate.
|
||||||
CONSUMED_STATUSES: tuple[str, ...] = ("queued", "sending", "sent")
|
CONSUMED_STATUSES: tuple[str, ...] = ("queued", "sending", "sent")
|
||||||
@@ -30,7 +29,7 @@ CONSUMED_STATUSES: tuple[str, ...] = ("queued", "sending", "sent")
|
|||||||
# monthly_limit -- None = nelimitat; int = limita prestatii/luna
|
# monthly_limit -- None = nelimitat; int = limita prestatii/luna
|
||||||
# api_access -- True = acces import prin API (/v1/*); False = doar web dashboard
|
# api_access -- True = acces import prin API (/v1/*); False = doar web dashboard
|
||||||
#
|
#
|
||||||
# Aliniat landing-ului comercial (PRD 5.17 US-001):
|
# Aliniat landing-ului comercial:
|
||||||
# Gratuit: 60/luna, fara API
|
# Gratuit: 60/luna, fara API
|
||||||
# Standard: nelimitat, fara API
|
# Standard: nelimitat, fara API
|
||||||
# Pro: nelimitat, cu API
|
# Pro: nelimitat, cu API
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Medii RAR per cont (PRD 5.20): disponibilitate + default efectiv.
|
"""Medii RAR per cont: disponibilitate + default efectiv.
|
||||||
|
|
||||||
Sursa UNICA de adevar pentru REQ-DISP / REQ-DEFAULT: vizibilitatea selector/toggle
|
Sursa UNICA de adevar pentru REQ-DISP / REQ-DEFAULT: vizibilitatea selector/toggle
|
||||||
in UI, validarea tintei in API si decizia worker-ului citesc TOATE de aici, ca sa
|
in UI, validarea tintei in API si decizia worker-ului citesc TOATE de aici, ca sa
|
||||||
@@ -92,7 +92,7 @@ def rar_env_efectiv_cont(conn: sqlite3.Connection, account_id: int) -> str | Non
|
|||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------------- #
|
# --------------------------------------------------------------------------- #
|
||||||
# Exceptie si rezolvator de mediu tinta (US-004, dependent de US-002) #
|
# Exceptie si rezolvator de mediu tinta #
|
||||||
# --------------------------------------------------------------------------- #
|
# --------------------------------------------------------------------------- #
|
||||||
|
|
||||||
class MediuIndisponibil(Exception):
|
class MediuIndisponibil(Exception):
|
||||||
@@ -126,7 +126,7 @@ def rezolva_rar_env(
|
|||||||
4. `cerut` None -> incearca rar_env_efectiv_cont (default-ul contului).
|
4. `cerut` None -> incearca rar_env_efectiv_cont (default-ul contului).
|
||||||
5. Daca contul nu are niciun mediu disponibil (rar_env_efectiv_cont == None)
|
5. Daca contul nu are niciun mediu disponibil (rar_env_efectiv_cont == None)
|
||||||
-> cade pe ancora globala get_settings().rar_env, normalizata la VALID_ENVS.
|
-> cade pe ancora globala get_settings().rar_env, normalizata la VALID_ENVS.
|
||||||
Acest fallback e intentionat (PRD 5.20 §2 Non-Goals): AUTOPASS_RAR_ENV ramane
|
Acest fallback e intentionat: AUTOPASS_RAR_ENV ramane
|
||||||
ancora de migrare si fallback pentru actiuni fara cont (keepalive, canal API cu
|
ancora de migrare si fallback pentru actiuni fara cont (keepalive, canal API cu
|
||||||
creds efemere pe conturi nou-create fara medii configurate).
|
creds efemere pe conturi nou-create fara medii configurate).
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
-- Schema SQLite (WAL) pentru gateway RAR AUTOPASS.
|
-- Schema SQLite (WAL) pentru gateway RAR AUTOPASS.
|
||||||
-- Vezi plan.md sect. 5 + plan-treapta2.md sect. 4.
|
-- Conturi cu creds RAR durabile, tabele import, atestari.
|
||||||
-- Treapta 2: adauga conturi cu creds RAR durabile, tabele import, atestari.
|
|
||||||
|
|
||||||
PRAGMA journal_mode = WAL;
|
PRAGMA journal_mode = WAL;
|
||||||
PRAGMA foreign_keys = ON;
|
PRAGMA foreign_keys = ON;
|
||||||
@@ -10,7 +9,7 @@ CREATE TABLE IF NOT EXISTS accounts (
|
|||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
cui TEXT,
|
cui TEXT,
|
||||||
email TEXT, -- email canonic de contact al firmei (US-001, PRD 5.12); nullable pt. conturi legacy
|
email TEXT, -- email canonic de contact al firmei; nullable pt. conturi legacy
|
||||||
active INTEGER NOT NULL DEFAULT 1, -- lifecycle cont (3.1); gate „in asteptare" consumat de 3.3
|
active INTEGER NOT NULL DEFAULT 1, -- lifecycle cont (3.1); gate „in asteptare" consumat de 3.3
|
||||||
-- Stare de ciclu de viata explicita (5.5). Superset al lui `active`: mentinem invariantul
|
-- Stare de ciclu de viata explicita (5.5). Superset al lui `active`: mentinem invariantul
|
||||||
-- active=1 <=> status='active' (vezi accounts.set_status / set_active). Worker gate-uieste pe status.
|
-- active=1 <=> status='active' (vezi accounts.set_status / set_active). Worker gate-uieste pe status.
|
||||||
@@ -19,8 +18,8 @@ CREATE TABLE IF NOT EXISTS accounts (
|
|||||||
-- vezi accounts.delete_account — randul ramane doar pentru audit).
|
-- vezi accounts.delete_account — randul ramane doar pentru audit).
|
||||||
status TEXT NOT NULL DEFAULT 'active'
|
status TEXT NOT NULL DEFAULT 'active'
|
||||||
CHECK (status IN ('pending','active','blocked','archived','deleted')),
|
CHECK (status IN ('pending','active','blocked','archived','deleted')),
|
||||||
-- Medii RAR per cont (PRD 5.20 US-001). Fiecare mediu = bifa de activare + slot creds.
|
-- Medii RAR per cont. Fiecare mediu = bifa de activare + slot creds.
|
||||||
-- accounts.rar_creds_enc (legacy env-less) a fost dropata in US-013 (SQLite DROP COLUMN).
|
-- accounts.rar_creds_enc (legacy env-less) a fost dropata (SQLite DROP COLUMN).
|
||||||
-- medii_disponibile = enabled AND creds prezente (app/rar_env.py). Cont client nou =
|
-- medii_disponibile = enabled AND creds prezente (app/rar_env.py). Cont client nou =
|
||||||
-- Productie on / Testare off (clientii declara real); contul operator se pune manual pe Testare.
|
-- Productie on / Testare off (clientii declara real); contul operator se pune manual pe Testare.
|
||||||
rar_test_enabled INTEGER NOT NULL DEFAULT 0 CHECK (rar_test_enabled IN (0, 1)),
|
rar_test_enabled INTEGER NOT NULL DEFAULT 0 CHECK (rar_test_enabled IN (0, 1)),
|
||||||
@@ -33,7 +32,7 @@ CREATE TABLE IF NOT EXISTS accounts (
|
|||||||
-- 1 (respinge cererea fara enqueue). Override per-cerere via PrezentareRequest.on_unmapped_error.
|
-- 1 (respinge cererea fara enqueue). Override per-cerere via PrezentareRequest.on_unmapped_error.
|
||||||
on_unmapped_error_default INTEGER NOT NULL DEFAULT 0
|
on_unmapped_error_default INTEGER NOT NULL DEFAULT 0
|
||||||
CHECK (on_unmapped_error_default IN (0, 1)),
|
CHECK (on_unmapped_error_default IN (0, 1)),
|
||||||
-- Comutator Auto per cont (PRD 5.19 US-001). AL TREILEA `auto_send` din schema (R6):
|
-- Comutator Auto per cont. AL TREILEA `auto_send` din schema:
|
||||||
-- DISTINCT de operations_mapping.auto_send si operation_text_rules.auto_send, care sunt
|
-- DISTINCT de operations_mapping.auto_send si operation_text_rules.auto_send, care sunt
|
||||||
-- PER-OPERATIE. Acesta e PER-CONT si guverneaza DOAR: (a) valoarea implicita a lui
|
-- PER-OPERATIE. Acesta e PER-CONT si guverneaza DOAR: (a) valoarea implicita a lui
|
||||||
-- submissions.held la ingestie (held = 0 daca 1, altfel 1 — vezi accounts.held_for_account);
|
-- submissions.held la ingestie (held = 0 daca 1, altfel 1 — vezi accounts.held_for_account);
|
||||||
@@ -104,7 +103,7 @@ CREATE TABLE IF NOT EXISTS submissions (
|
|||||||
status TEXT NOT NULL DEFAULT 'queued'
|
status TEXT NOT NULL DEFAULT 'queued'
|
||||||
CHECK (status IN ('queued','sending','sent','needs_mapping','needs_data','error')),
|
CHECK (status IN ('queued','sending','sent','needs_mapping','needs_data','error')),
|
||||||
payload_json TEXT NOT NULL,
|
payload_json TEXT NOT NULL,
|
||||||
-- Mediul RAR tinta al acestei trimiteri (PRD 5.20 US-001). DEFAULT 'test' e doar plasa
|
-- Mediul RAR tinta al acestei trimiteri. DEFAULT 'test' e doar plasa
|
||||||
-- pentru randuri net-noi care nu seteaza explicit; fiecare INSERT (API/import/web) seteaza
|
-- pentru randuri net-noi care nu seteaza explicit; fiecare INSERT (API/import/web) seteaza
|
||||||
-- rar_env explicit. Backfill din AUTOPASS_RAR_ENV global la migrare (NU lasa pe DEFAULT).
|
-- rar_env explicit. Backfill din AUTOPASS_RAR_ENV global la migrare (NU lasa pe DEFAULT).
|
||||||
rar_env TEXT NOT NULL DEFAULT 'test' CHECK (rar_env IN ('test', 'prod')),
|
rar_env TEXT NOT NULL DEFAULT 'test' CHECK (rar_env IN ('test', 'prod')),
|
||||||
@@ -113,12 +112,12 @@ CREATE TABLE IF NOT EXISTS submissions (
|
|||||||
rar_error TEXT,
|
rar_error TEXT,
|
||||||
id_prezentare INTEGER, -- data.id intors de RAR la succes
|
id_prezentare INTEGER, -- data.id intors de RAR la succes
|
||||||
retry_count INTEGER NOT NULL DEFAULT 0,
|
retry_count INTEGER NOT NULL DEFAULT 0,
|
||||||
next_attempt_at TEXT, -- backoff: randul nu se ia inainte de acest moment (T2)
|
next_attempt_at TEXT, -- backoff: randul nu se ia inainte de acest moment
|
||||||
sending_since TEXT, -- pentru lease/timeout pe randuri 'sending' orfane (T2)
|
sending_since TEXT, -- pentru lease/timeout pe randuri 'sending' orfane
|
||||||
purge_after TEXT, -- sent + 90z (T16)
|
purge_after TEXT, -- sent + 90z
|
||||||
batch_id INTEGER, -- import batch (T7; NULL = canal API)
|
batch_id INTEGER, -- import batch (NULL = canal API)
|
||||||
row_index INTEGER, -- rand in batch (T7; NULL = canal API)
|
row_index INTEGER, -- rand in batch (NULL = canal API)
|
||||||
-- Randuri tinute manual (PRD 5.19 US-001). rand `queued AND held=1` = "In asteptare
|
-- Randuri tinute manual. rand `queued AND held=1` = "In asteptare
|
||||||
-- (manual)": tinut pana la eliberare umana (trimitere manuala per rand/bulk: held 1 -> 0).
|
-- (manual)": tinut pana la eliberare umana (trimitere manuala per rand/bulk: held 1 -> 0).
|
||||||
-- Worker-ul (claim_one) ia doar `queued AND held=0`. held NU intra in payload/idempotenta
|
-- Worker-ul (claim_one) ia doar `queued AND held=0`. held NU intra in payload/idempotenta
|
||||||
-- si NU pleaca la RAR — e pur control de coada (ca import_rows.reviewed). Snapshot la
|
-- si NU pleaca la RAR — e pur control de coada (ca import_rows.reviewed). Snapshot la
|
||||||
@@ -134,7 +133,7 @@ CREATE INDEX IF NOT EXISTS idx_submissions_account_status ON submissions(account
|
|||||||
-- ruleaza executescript(schema.sql) INAINTE de _migrate; pe un DB existent pre-`held`/`batch_id`
|
-- ruleaza executescript(schema.sql) INAINTE de _migrate; pe un DB existent pre-`held`/`batch_id`
|
||||||
-- coloana inca nu exista in momentul executarii schemei, deci un CREATE INDEX pe ea ar esua
|
-- coloana inca nu exista in momentul executarii schemei, deci un CREATE INDEX pe ea ar esua
|
||||||
-- (no such column). Indexul partial `idx_submissions_held ON submissions(held) WHERE held=1`
|
-- (no such column). Indexul partial `idx_submissions_held ON submissions(held) WHERE held=1`
|
||||||
-- (PRD 5.19 US-001) traieste doar in _migrate, ca `idx_submissions_batch`.
|
-- traieste doar in _migrate, ca `idx_submissions_batch`.
|
||||||
|
|
||||||
-- Mapare coloane fisier -> campuri canonice (retinuta per cont, semnatura coloane).
|
-- Mapare coloane fisier -> campuri canonice (retinuta per cont, semnatura coloane).
|
||||||
CREATE TABLE IF NOT EXISTS column_mappings (
|
CREATE TABLE IF NOT EXISTS column_mappings (
|
||||||
@@ -162,7 +161,7 @@ CREATE TABLE IF NOT EXISTS import_batches (
|
|||||||
already_sent INTEGER NOT NULL DEFAULT 0,
|
already_sent INTEGER NOT NULL DEFAULT 0,
|
||||||
duplicate_in_file INTEGER NOT NULL DEFAULT 0,
|
duplicate_in_file INTEGER NOT NULL DEFAULT 0,
|
||||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
purge_after TEXT -- created_at + 90z (T16)
|
purge_after TEXT -- created_at + 90z
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Randuri din lot de import (PII criptat cu Fernet).
|
-- Randuri din lot de import (PII criptat cu Fernet).
|
||||||
@@ -171,8 +170,8 @@ CREATE TABLE IF NOT EXISTS import_rows (
|
|||||||
batch_id INTEGER NOT NULL REFERENCES import_batches(id) ON DELETE CASCADE,
|
batch_id INTEGER NOT NULL REFERENCES import_batches(id) ON DELETE CASCADE,
|
||||||
row_index INTEGER NOT NULL,
|
row_index INTEGER NOT NULL,
|
||||||
raw_json TEXT NOT NULL, -- PII criptat (Fernet, ca submissions)
|
raw_json TEXT NOT NULL, -- PII criptat (Fernet, ca submissions)
|
||||||
override_json TEXT, -- patch CANONIC editat in preview, criptat Fernet (3.6, Approach B); NULL = fara editare
|
override_json TEXT, -- patch CANONIC editat in preview, criptat Fernet; NULL = fara editare
|
||||||
reviewed INTEGER NOT NULL DEFAULT 0, -- US-007 (PRD 5.12): 0=neconfirmat, 1=confirmat de operator; NU intra in payload/idempotenta
|
reviewed INTEGER NOT NULL DEFAULT 0, -- 0=neconfirmat, 1=confirmat de operator; NU intra in payload/idempotenta
|
||||||
resolved_status TEXT NOT NULL DEFAULT 'pending'
|
resolved_status TEXT NOT NULL DEFAULT 'pending'
|
||||||
CHECK (resolved_status IN (
|
CHECK (resolved_status IN (
|
||||||
'pending','ok','needs_mapping','needs_data',
|
'pending','ok','needs_mapping','needs_data',
|
||||||
@@ -207,15 +206,15 @@ CREATE TABLE IF NOT EXISTS users (
|
|||||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Jurnal de aplicatie la nivel de eveniment (PRD 5.6). Dublu canal: aici (vizibil
|
-- Jurnal de aplicatie la nivel de eveniment. Dublu canal: aici (vizibil
|
||||||
-- in tab "Jurnal") + log text rotativ (depanare). `tip` e text liber documentat
|
-- in tab "Jurnal") + log text rotativ (depanare). `tip` e text liber documentat
|
||||||
-- (lista extensibila, decizie §5) — adaugam tipuri fara migrare. Toate valorile
|
-- (lista extensibila) — adaugam tipuri fara migrare. Toate valorile
|
||||||
-- sunt REDACTATE la scriere (app/observ.py via app/security.py): parole/token ->
|
-- sunt REDACTATE la scriere (app/observ.py via app/security.py): parole/token ->
|
||||||
-- ***REDACTED***, VIN/nr partial. `context_json` = metadate (NU payload PII integral).
|
-- ***REDACTED***, VIN/nr partial. `context_json` = metadate (NU payload PII integral).
|
||||||
CREATE TABLE IF NOT EXISTS app_events (
|
CREATE TABLE IF NOT EXISTS app_events (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
ts TEXT NOT NULL DEFAULT (datetime('now')),
|
ts TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
request_id TEXT, -- corelare cu raspunsul clientului (US-002)
|
request_id TEXT, -- corelare cu raspunsul clientului
|
||||||
account_id INTEGER, -- NULL = eveniment de sistem (fara cont)
|
account_id INTEGER, -- NULL = eveniment de sistem (fara cont)
|
||||||
sursa TEXT NOT NULL DEFAULT 'api'
|
sursa TEXT NOT NULL DEFAULT 'api'
|
||||||
CHECK (sursa IN ('api','worker')),
|
CHECK (sursa IN ('api','worker')),
|
||||||
@@ -224,14 +223,14 @@ CREATE TABLE IF NOT EXISTS app_events (
|
|||||||
cod TEXT, -- cod din catalogul de erori (app/errors.py) daca aplica
|
cod TEXT, -- cod din catalogul de erori (app/errors.py) daca aplica
|
||||||
mesaj TEXT, -- mesaj scurt redactat
|
mesaj TEXT, -- mesaj scurt redactat
|
||||||
context_json TEXT, -- JSON metadate redactate (submission_id, count, status...)
|
context_json TEXT, -- JSON metadate redactate (submission_id, count, status...)
|
||||||
purge_after TEXT -- ts + log_retention_days (US-008)
|
purge_after TEXT -- ts + log_retention_days
|
||||||
);
|
);
|
||||||
CREATE INDEX IF NOT EXISTS idx_app_events_ts ON app_events(ts);
|
CREATE INDEX IF NOT EXISTS idx_app_events_ts ON app_events(ts);
|
||||||
CREATE INDEX IF NOT EXISTS idx_app_events_account ON app_events(account_id, ts);
|
CREATE INDEX IF NOT EXISTS idx_app_events_account ON app_events(account_id, ts);
|
||||||
CREATE INDEX IF NOT EXISTS idx_app_events_tip ON app_events(tip);
|
CREATE INDEX IF NOT EXISTS idx_app_events_tip ON app_events(tip);
|
||||||
|
|
||||||
-- Reguli automate de mapare pe text (substring, per cont). PRD 5.8 US-001.
|
-- Reguli automate de mapare pe text (substring, per cont).
|
||||||
-- auto_send DEFAULT 0 (decizie CEO 2026-06-24): substring are blast-radius mai mare
|
-- auto_send DEFAULT 0: substring are blast-radius mai mare
|
||||||
-- decat maparea exacta; o regula noua rezolva codul dar tine randul pentru verificare
|
-- decat maparea exacta; o regula noua rezolva codul dar tine randul pentru verificare
|
||||||
-- umana pana cand operatorul activeaza explicit "In coada".
|
-- umana pana cand operatorul activeaza explicit "In coada".
|
||||||
CREATE TABLE IF NOT EXISTS operation_text_rules (
|
CREATE TABLE IF NOT EXISTS operation_text_rules (
|
||||||
@@ -246,7 +245,7 @@ CREATE TABLE IF NOT EXISTS operation_text_rules (
|
|||||||
);
|
);
|
||||||
CREATE INDEX IF NOT EXISTS idx_text_rules_account ON operation_text_rules(account_id);
|
CREATE INDEX IF NOT EXISTS idx_text_rules_account ON operation_text_rules(account_id);
|
||||||
|
|
||||||
-- Sugestii de mapare (strat SILVER, L14-S3 PRD 5.14).
|
-- Sugestii de mapare (strat SILVER).
|
||||||
-- Etichete LLM/embedding — bootstrap; citita DOAR de suggest_codes/pending_unmapped,
|
-- Etichete LLM/embedding — bootstrap; citita DOAR de suggest_codes/pending_unmapped,
|
||||||
-- NICIODATA de load_mapping/resolve_prestatii (separare structurala #13).
|
-- NICIODATA de load_mapping/resolve_prestatii (separare structurala #13).
|
||||||
-- Cheia = denumire normalizata (fara diacritice, uppercase, spatii colapsate).
|
-- Cheia = denumire normalizata (fara diacritice, uppercase, spatii colapsate).
|
||||||
@@ -265,7 +264,7 @@ CREATE TABLE IF NOT EXISTS mapping_suggestions (
|
|||||||
CREATE INDEX IF NOT EXISTS idx_mapping_suggestions_cod
|
CREATE INDEX IF NOT EXISTS idx_mapping_suggestions_cod
|
||||||
ON mapping_suggestions(cod_prestatie) WHERE cod_prestatie IS NOT NULL;
|
ON mapping_suggestions(cod_prestatie) WHERE cod_prestatie IS NOT NULL;
|
||||||
|
|
||||||
-- Mapari validate de oameni (strat GOLD partajat cross-account, L14-S3 PRD 5.14).
|
-- Mapari validate de oameni (strat GOLD partajat cross-account).
|
||||||
-- Confirmarile umane din ORICE cont contribuie la acest store (#8).
|
-- Confirmarile umane din ORICE cont contribuie la acest store (#8).
|
||||||
-- cross-account = suggestion-only (pre-completeaza editorul, F-A/#11), NU auto-send.
|
-- cross-account = suggestion-only (pre-completeaza editorul, F-A/#11), NU auto-send.
|
||||||
-- Auto-send DOAR din operations_mapping (GOLD propriu per-cont, Eng-F2).
|
-- Auto-send DOAR din operations_mapping (GOLD propriu per-cont, Eng-F2).
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
"""Store partajat pentru sugestii (SILVER) si mapari validate de oameni (GOLD cross-account).
|
"""Store partajat pentru sugestii (SILVER) si mapari validate de oameni (GOLD cross-account).
|
||||||
|
|
||||||
Straturi (L14-S3, PRD 5.14):
|
Straturi:
|
||||||
- mapping_suggestions (SILVER): sugestii LLM/embedding, citite DOAR de suggest/pending_unmapped,
|
- mapping_suggestions (SILVER): sugestii LLM/embedding, citite DOAR de suggest/pending_unmapped,
|
||||||
NICIODATA de load_mapping/resolve_prestatii (separare structurala #13).
|
NICIODATA de load_mapping/resolve_prestatii (separare structurala).
|
||||||
- shared_mappings (GOLD partajat): mapari validate de om din orice cont; pot pre-completa
|
- shared_mappings (GOLD partajat): mapari validate de om din orice cont; pot pre-completa
|
||||||
editorul (suggestion-only cross-account, F-A/#11); auto-send DOAR GOLD propriu
|
editorul (suggestion-only cross-account); auto-send DOAR GOLD propriu
|
||||||
(operations_mapping per-cont, Eng-F2).
|
(operations_mapping per-cont).
|
||||||
|
|
||||||
Invariante:
|
Invariante:
|
||||||
- INSERT OR IGNORE la seed: nu suprascrie randuri existente (#2).
|
- INSERT OR IGNORE la seed: nu suprascrie randuri existente.
|
||||||
- NUL = is_nul=1, cod_prestatie NULL; NU se promoveaza la cod RAR (#4).
|
- NUL = is_nul=1, cod_prestatie NULL; NU se promoveaza la cod RAR.
|
||||||
- source/confidence pe fiecare rand (provenienta + rollback batch model prost, #5).
|
- source/confidence pe fiecare rand (provenienta + rollback batch model prost).
|
||||||
- Wiring in resolve_prestatii/load_mapping vine in L14-S6; modulul de fata e API pur.
|
- Wiring in resolve_prestatii/load_mapping nu e cablat inca; modulul de fata e API pur.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ def requeue_submission(conn, account_id: int, sid: int) -> dict:
|
|||||||
valid_codes = load_nomenclator_codes(conn) or None
|
valid_codes = load_nomenclator_codes(conn) or None
|
||||||
cl = classify_prezentare(content, mapping, mapping_meta, valid_codes)
|
cl = classify_prezentare(content, mapping, mapping_meta, valid_codes)
|
||||||
|
|
||||||
# PRD 5.19 US-002: re-snapshot `held` din comutatorul contului la re-punere in coada
|
# Re-snapshot `held` din comutatorul contului la re-punere in coada
|
||||||
# (paritate cu create_prezentari/reresolve_account). Fara asta un rand repus pastra
|
# (paritate cu create_prezentari/reresolve_account). Fara asta un rand repus pastra
|
||||||
# `held` VECHI si s-ar auto-trimite desi contul e Auto OFF (sau ar ramane blocat cand e ON).
|
# `held` VECHI si s-ar auto-trimite desi contul e Auto OFF (sau ar ramane blocat cand e ON).
|
||||||
held_val = held_for_account(conn, account_or_default(account_id))
|
held_val = held_for_account(conn, account_or_default(account_id))
|
||||||
|
|||||||
47
app/users.py
47
app/users.py
@@ -157,6 +157,53 @@ def verify_password(conn: sqlite3.Connection, email: str, password: str) -> int
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def change_password(
|
||||||
|
conn: sqlite3.Connection,
|
||||||
|
user_id: int,
|
||||||
|
current_password: str,
|
||||||
|
new_password: str,
|
||||||
|
) -> None:
|
||||||
|
"""Schimba parola de acces a userului identificat prin user_id.
|
||||||
|
|
||||||
|
Verifica parola actuala (scrypt, comparatie constant-time), valideaza parola
|
||||||
|
noua (10..128, diferita de cea actuala) si rescrie password_hash + salt nou cu
|
||||||
|
parametrii scrypt curenti. Parola NICIODATA stocata in clar.
|
||||||
|
|
||||||
|
Ridica ValueError la: user inexistent, parametri parola corupti, parola actuala
|
||||||
|
gresita, parola noua invalida. Nu invalideaza sesiunea (apelantul decide).
|
||||||
|
"""
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT password_hash, salt, scrypt_params FROM users WHERE id=?",
|
||||||
|
(user_id,),
|
||||||
|
).fetchone()
|
||||||
|
if row is None:
|
||||||
|
raise ValueError("utilizator inexistent")
|
||||||
|
|
||||||
|
salt = bytes.fromhex(row["salt"])
|
||||||
|
expected = bytes.fromhex(row["password_hash"])
|
||||||
|
params = _parse_scrypt_params(row["scrypt_params"] or "")
|
||||||
|
if params is None:
|
||||||
|
raise ValueError("parametri parola corupti")
|
||||||
|
n, r, p = params
|
||||||
|
actual = _scrypt_hash(current_password, salt, n=n, r=r, p=p)
|
||||||
|
if not hmac.compare_digest(actual, expected):
|
||||||
|
raise ValueError("Parola actuala este incorecta.")
|
||||||
|
|
||||||
|
if len(new_password) < _PASSWORD_MIN:
|
||||||
|
raise ValueError(f"Parola noua e prea scurta (minim {_PASSWORD_MIN} caractere).")
|
||||||
|
if len(new_password) > _PASSWORD_MAX:
|
||||||
|
raise ValueError(f"Parola noua e prea lunga (maxim {_PASSWORD_MAX} caractere).")
|
||||||
|
if new_password == current_password:
|
||||||
|
raise ValueError("Parola noua trebuie sa difere de cea actuala.")
|
||||||
|
|
||||||
|
new_salt = secrets.token_bytes(16)
|
||||||
|
new_hash = _scrypt_hash(new_password, new_salt)
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE users SET password_hash=?, salt=?, scrypt_params=? WHERE id=?",
|
||||||
|
(new_hash.hex(), new_salt.hex(), SCRYPT_PARAMS, user_id),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_user_by_email(conn: sqlite3.Connection, email: str) -> dict | None:
|
def get_user_by_email(conn: sqlite3.Connection, email: str) -> dict | None:
|
||||||
"""Metadate user dupa email (FARA password_hash si salt)."""
|
"""Metadate user dupa email (FARA password_hash si salt)."""
|
||||||
row = conn.execute(
|
row = conn.execute(
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Validare de domeniu pentru prezentari (T3).
|
"""Validare de domeniu pentru prezentari.
|
||||||
|
|
||||||
Replica regulile RAR (docs/api-rar-contract.md sect. "Reguli de validare") ÎNAINTE
|
Replica regulile RAR (docs/api-rar-contract.md sect. "Reguli de validare") ÎNAINTE
|
||||||
de enqueue, ca sa nu primim 4xx de la RAR. Spre deosebire de validarea de SHAPE
|
de enqueue, ca sa nu primim 4xx de la RAR. Spre deosebire de validarea de SHAPE
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ def _apply_lifecycle(conn, ids: list[int], action: str) -> None:
|
|||||||
for aid in ids:
|
for aid in ids:
|
||||||
try:
|
try:
|
||||||
if action == "activate":
|
if action == "activate":
|
||||||
# Gate US-002: nu activam conturi fara identitate completa (companie+email+CUI)
|
# Nu activam conturi fara identitate completa (companie+email+CUI)
|
||||||
acct_row = conn.execute(
|
acct_row = conn.execute(
|
||||||
"SELECT id, name, cui, email FROM accounts WHERE id=?", (aid,)
|
"SELECT id, name, cui, email FROM accounts WHERE id=?", (aid,)
|
||||||
).fetchone()
|
).fetchone()
|
||||||
|
|||||||
@@ -79,7 +79,6 @@ async def signup_post(
|
|||||||
name=name, cui=cui, email=email, plan=requested_plan,
|
name=name, cui=cui, email=email, plan=requested_plan,
|
||||||
), status_code=422)
|
), status_code=422)
|
||||||
|
|
||||||
# CUI obligatoriu la signup (US-001, PRD 5.12)
|
|
||||||
cui_norm = cui.strip().upper() if cui else ""
|
cui_norm = cui.strip().upper() if cui else ""
|
||||||
if not cui_norm:
|
if not cui_norm:
|
||||||
return _TMPL.TemplateResponse(request, "signup.html", _ctx(
|
return _TMPL.TemplateResponse(request, "signup.html", _ctx(
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ ETICHETE_SCURTE: dict[str, str] = {
|
|||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Randuri tinute manual (held) — US-006, PRD 5.19
|
# Randuri tinute manual (held).
|
||||||
#
|
#
|
||||||
# `held` NU e o stare noua in masina de stari: un rand cu `status='queued' AND
|
# `held` NU e o stare noua in masina de stari: un rand cu `status='queued' AND
|
||||||
# held=1` este in coada, dar tinut pana la eliberarea umana. Il afisam distinct
|
# held=1` este in coada, dar tinut pana la eliberarea umana. Il afisam distinct
|
||||||
@@ -96,9 +96,8 @@ def eticheta_scurta(status: str, held: bool = False) -> str:
|
|||||||
Returneaza eticheta compacta (pill) pentru o stare de submission.
|
Returneaza eticheta compacta (pill) pentru o stare de submission.
|
||||||
|
|
||||||
Cand `status='queued'` si `held` este adevarat, randul e tinut manual si
|
Cand `status='queued'` si `held` este adevarat, randul e tinut manual si
|
||||||
intoarce "Manual" (distinct de "In coada") — vezi PRD 5.19 D5. Pentru orice
|
intoarce "Manual" (distinct de "In coada"). Pentru orice alta stare `held`
|
||||||
alta stare `held` este ignorat, iar apelul fara `held` ramane identic cu cel
|
este ignorat.
|
||||||
dinainte de US-006.
|
|
||||||
|
|
||||||
Arunca KeyError daca starea nu este mapata — intentionat, ca sa prinda
|
Arunca KeyError daca starea nu este mapata — intentionat, ca sa prinda
|
||||||
stari noi adaugate in schema fara mapare corespunzatoare.
|
stari noi adaugate in schema fara mapare corespunzatoare.
|
||||||
@@ -120,8 +119,7 @@ def eticheta_stare(status: str, held: bool = False) -> Eticheta:
|
|||||||
|
|
||||||
Cand `status='queued'` si `held` este adevarat, randul e tinut manual si
|
Cand `status='queued'` si `held` este adevarat, randul e tinut manual si
|
||||||
intoarce eticheta "In asteptare (manual)" cu clasa de avertizare (amber /
|
intoarce eticheta "In asteptare (manual)" cu clasa de avertizare (amber /
|
||||||
--warn) — vezi PRD 5.19 US-006/D5. Pentru orice alta stare `held` este
|
--warn). Pentru orice alta stare `held` este ignorat.
|
||||||
ignorat, iar apelul fara `held` ramane identic cu cel dinainte de US-006.
|
|
||||||
|
|
||||||
Arunca KeyError daca starea nu este mapata — intentionat, ca sa prinda
|
Arunca KeyError daca starea nu este mapata — intentionat, ca sa prinda
|
||||||
stari noi adaugate in schema fara mapare corespunzatoare.
|
stari noi adaugate in schema fara mapare corespunzatoare.
|
||||||
@@ -385,7 +383,7 @@ def nota_umana_preview(status: str, errors: list, flags: list) -> str:
|
|||||||
|
|
||||||
Primeste ``errors`` ca lista Python (nu JSON string) — NU pasa la motiv_uman
|
Primeste ``errors`` ca lista Python (nu JSON string) — NU pasa la motiv_uman
|
||||||
sau parse_erori care asteapta un JSON string si ar produce repr Python brut
|
sau parse_erori care asteapta un JSON string si ar produce repr Python brut
|
||||||
prin fallback ``raw[:160]`` (bug documentat in PRD 5.11 US-003).
|
prin fallback ``raw[:160]``.
|
||||||
|
|
||||||
Logica de prioritate:
|
Logica de prioritate:
|
||||||
- already_sent / duplicate_in_file -> "" (template le afiseaza separat)
|
- already_sent / duplicate_in_file -> "" (template le afiseaza separat)
|
||||||
@@ -436,7 +434,7 @@ def nota_umana_preview(status: str, errors: list, flags: list) -> str:
|
|||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Etichete mediu RAR (Test / Productie) — US-010 PRD 5.20
|
# Etichete mediu RAR (Test / Productie)
|
||||||
#
|
#
|
||||||
# Conventia de culori (semantica risc L.142):
|
# Conventia de culori (semantica risc L.142):
|
||||||
# prod = env-badge-prod (fill atentie, text alb) — declaratie REALA, ireversibila.
|
# prod = env-badge-prod (fill atentie, text alb) — declaratie REALA, ireversibila.
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
<div id="acasa-section">
|
<div id="acasa-section">
|
||||||
|
|
||||||
{# === Banner ne-blocant: cont incomplet (US-002) ===
|
{# === Banner ne-blocant: cont incomplet ===
|
||||||
Apare cand accounts.name / email / CUI sunt necompletate (conturi legacy sau create din CLI).
|
Apare cand accounts.name / email / CUI sunt necompletate (conturi legacy sau create din CLI).
|
||||||
NU blocheaza importul sau uploadul — doar orienteaza operatorul sa completeze datele.
|
NU blocheaza importul sau uploadul — doar orienteaza operatorul sa completeze datele.
|
||||||
Dispare automat dupa ce contul devine complet (re-render la urmatoarea navigare/reload).
|
Dispare automat dupa ce contul devine complet (re-render la urmatoarea navigare/reload).
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{# === Container colapsabil: stepper + upload intr-un singur element <details> (US-006).
|
{# === Container colapsabil: stepper + upload intr-un singur element <details>.
|
||||||
Serverul seteaza atributul `open` din are_trimiteri:
|
Serverul seteaza atributul `open` din are_trimiteri:
|
||||||
are_trimiteri=False (first-run) → open (importul e vizibil imediat, fara JS)
|
are_trimiteri=False (first-run) → open (importul e vizibil imediat, fara JS)
|
||||||
are_trimiteri=True (returning) → colapsat (nu ocupa ecranul, dar e accesibil la click)
|
are_trimiteri=True (returning) → colapsat (nu ocupa ecranul, dar e accesibil la click)
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
{#
|
{#
|
||||||
_auto_send_toggle.html — comutatorul "Trimite automat la RAR" (PRD 5.19 US-004).
|
_auto_send_toggle.html — comutatorul "Trimite automat la RAR".
|
||||||
|
|
||||||
Plasat in clusterul de header (base.html, langa .rar-chip) — D1. NU face parte din
|
Plasat in clusterul de header (base.html, langa .rar-chip). NU face parte din
|
||||||
swap-ul periodic al #status-bar (every 15s) -> fara flicker / pierdere focus (D3).
|
swap-ul periodic al #status-bar (every 15s) -> fara flicker / pierdere focus.
|
||||||
|
|
||||||
Non-optimist (D2): la esec POST bifa revine + toast eroare (hx-on::response-error).
|
Non-optimist: la esec POST bifa revine + toast eroare (hx-on::response-error).
|
||||||
account_id vine din SESIUNE server-side (ruta /auto-send), niciodata din formular.
|
account_id vine din SESIUNE server-side (ruta /auto-send), niciodata din formular.
|
||||||
|
|
||||||
Raspunsul POST re-randeaza acest fragment (outerHTML pe #auto-send-toggle-wrap):
|
Raspunsul POST re-randeaza acest fragment (outerHTML pe #auto-send-toggle-wrap):
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
{#
|
{#
|
||||||
_bulk_held_banner.html — banner bulk "Trimite toate (N)" (PRD 5.19 US-005).
|
_bulk_held_banner.html — banner bulk "Trimite toate (N)".
|
||||||
|
|
||||||
N = randuri tinute ale contului (queued AND held=1). Randat:
|
N = randuri tinute ale contului (queued AND held=1). Randat:
|
||||||
- initial in _coada.html (banner_oob nesetat -> fara OOB);
|
- initial in _coada.html (banner_oob nesetat -> fara OOB);
|
||||||
- RE-RANDAT prin OOB (banner_oob=true) din _submissions.html la FIECARE reincarcare a
|
- RE-RANDAT prin OOB (banner_oob=true) din _submissions.html la FIECARE reincarcare a
|
||||||
listei, ca sa nu ramana stale dupa eliberari (fix /code-review #3).
|
listei, ca sa nu ramana stale dupa eliberari.
|
||||||
|
|
||||||
Wrapper cu id STABIL #bulk-held-banner-wrap: OOB outerHTML il inlocuieste mereu (chiar si
|
Wrapper cu id STABIL #bulk-held-banner-wrap: OOB outerHTML il inlocuieste mereu (chiar si
|
||||||
gol la N=0), ca id-ul sa ramana tinta pentru urmatorul OOB. Confirmare tipata prin modal
|
gol la N=0), ca id-ul sa ramana tinta pentru urmatorul OOB. Confirmare tipata prin modal
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
{% set _sec_id = chips_section_id or 'chips-section' %}
|
{% set _sec_id = chips_section_id or 'chips-section' %}
|
||||||
{% set _chips = prestatii_chips or [] %}
|
{% set _chips = prestatii_chips or [] %}
|
||||||
{% set _has_ops = _chips | selectattr('cod_op_service') | list | length > 0 %}
|
{% set _has_ops = _chips | selectattr('cod_op_service') | list | length > 0 %}
|
||||||
{# US-009: chips_submission_id e setat din _detaliu_ctx cand chips sunt randate in modalul de detaliu.
|
{# chips_submission_id e setat din _detaliu_ctx cand chips sunt randate in modalul de detaliu.
|
||||||
Lipseste cand _chips_prestatii.html e rerandat via /form-chips (stateless, fara submission). #}
|
Lipseste cand _chips_prestatii.html e rerandat via /form-chips (stateless, fara submission). #}
|
||||||
{% set _sub_id = chips_submission_id if chips_submission_id is defined else none %}
|
{% set _sub_id = chips_submission_id if chips_submission_id is defined else none %}
|
||||||
|
|
||||||
@@ -72,7 +72,7 @@
|
|||||||
×
|
×
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
{# US-009: "salveaza ca regula op->cod" — apare doar cand submission_id e cunoscut
|
{# "salveaza ca regula op->cod" — apare doar cand submission_id e cunoscut
|
||||||
(in modalul de detaliu, nu la re-randarea stateless via /form-chips).
|
(in modalul de detaliu, nu la re-randarea stateless via /form-chips).
|
||||||
Reuse EXACT save_mapping + reresolve_account via endpoint dedicat.
|
Reuse EXACT save_mapping + reresolve_account via endpoint dedicat.
|
||||||
hx-include="closest form" propaga csrf_token din form-ul parinte. #}
|
hx-include="closest form" propaga csrf_token din form-ul parinte. #}
|
||||||
@@ -117,9 +117,9 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
{# ===== US-005 (5.16): Chips extra + picker '+ Adauga alta operatie / cod RAR' in mod operatii ===== #}
|
{# ===== Chips extra + picker '+ Adauga alta operatie / cod RAR' in mod operatii ===== #}
|
||||||
{# Chips extra: cod_op_service gol, cod_prestatie setat — afisate flat cu × (reuse remove_flat).
|
{# Chips extra: cod_op_service gol, cod_prestatie setat — afisate flat cu × (reuse remove_flat).
|
||||||
T-7 (5.16): containerul .chips se randeaza DOAR cand exista chips extra — altfel ramanea
|
Containerul .chips se randeaza DOAR cand exista chips extra — altfel ramanea
|
||||||
un chenar gol nefinisat sub randurile de operatie. #}
|
un chenar gol nefinisat sub randurile de operatie. #}
|
||||||
{% set _extra_chips = _chips | rejectattr('cod_op_service') | selectattr('cod_prestatie') | list %}
|
{% set _extra_chips = _chips | rejectattr('cod_op_service') | selectattr('cod_prestatie') | list %}
|
||||||
{% if _extra_chips %}
|
{% if _extra_chips %}
|
||||||
|
|||||||
@@ -6,13 +6,13 @@
|
|||||||
style="margin-top:22px; padding-top:18px; border-top:2px solid var(--line);"
|
style="margin-top:22px; padding-top:18px; border-top:2px solid var(--line);"
|
||||||
{% if oob %}hx-swap-oob="outerHTML"{% endif %}>
|
{% if oob %}hx-swap-oob="outerHTML"{% endif %}>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
{# US-002 (5.16): titlul de sectiune vizibil ("Trimiterile tale") a fost eliminat —
|
{# Titlul de sectiune vizibil ("Trimiterile tale") a fost eliminat — lista incepe
|
||||||
lista incepe direct sub filtre. Heading pastrat sr-only pentru a11y (section
|
direct sub filtre. Heading pastrat sr-only pentru a11y (section
|
||||||
aria-labelledby). Badge-ul de atentie + export CSV stau intr-un rand discret. #}
|
aria-labelledby). Badge-ul de atentie + export CSV stau intr-un rand discret. #}
|
||||||
<h2 id="trimiteri-heading" class="sr-only">Trimiterile tale</h2>
|
<h2 id="trimiteri-heading" class="sr-only">Trimiterile tale</h2>
|
||||||
|
|
||||||
{# US-005 (PRD 5.19): bulk "Trimite toate (N)". Randat initial aici; re-randat prin OOB
|
{# Bulk "Trimite toate (N)". Randat initial aici; re-randat prin OOB
|
||||||
din _submissions.html la fiecare reincarcare a listei (fix staleness /code-review #3). #}
|
din _submissions.html la fiecare reincarcare a listei (evita staleness). #}
|
||||||
{% include '_bulk_held_banner.html' %}
|
{% include '_bulk_held_banner.html' %}
|
||||||
|
|
||||||
{% if blocate_total %}
|
{% if blocate_total %}
|
||||||
@@ -44,8 +44,8 @@
|
|||||||
<input type="hidden" id="f-status" name="status" value="{{ status_filtru | default('', true) }}">
|
<input type="hidden" id="f-status" name="status" value="{{ status_filtru | default('', true) }}">
|
||||||
{# Pagina curenta — actualizata prin OOB swap din _submissions.html; inclusa la reincarcari. #}
|
{# Pagina curenta — actualizata prin OOB swap din _submissions.html; inclusa la reincarcari. #}
|
||||||
<input type="hidden" id="f-page" name="page" value="1">
|
<input type="hidden" id="f-page" name="page" value="1">
|
||||||
{# US-007 (5.19): filtrul "doar tinute" — persistat prin OOB din _submissions.html, ca sa
|
{# Filtrul "doar tinute" — persistat prin OOB din _submissions.html, ca sa
|
||||||
nu se piarda la reincarcare (trimiteriChanged) sau paginare (fix /code-review #2). #}
|
nu se piarda la reincarcare (trimiteriChanged) sau paginare. #}
|
||||||
<input type="hidden" id="f-held" name="held" value="{{ f_held | default('') }}">
|
<input type="hidden" id="f-held" name="held" value="{{ f_held | default('') }}">
|
||||||
|
|
||||||
{# === STANGA: Quick-pills de data (preset interval) + buton Custom ===
|
{# === STANGA: Quick-pills de data (preset interval) + buton Custom ===
|
||||||
|
|||||||
@@ -1,228 +1,235 @@
|
|||||||
<div class="card" id="card-cont">
|
{# Pagina Setari: sub-taburi interne — Date firma / Credentiale RAR / Cheie API /
|
||||||
<h2 style="font-size:15px; margin:0 0 16px;">Contul meu</h2>
|
Cont (plan + parola). `activ_subtab` (server) alege panoul activ dupa fiecare
|
||||||
|
swap HTMX, ca re-randarea sa nu resete la primul tab. Comutarea intre taburi
|
||||||
<!-- Sectiunea: Plan curent (US-006 PRD 5.17) -->
|
fara server = JS delegat in base.html (#card-cont). #}
|
||||||
{% if plan_linie is defined %}
|
{% set _firma_ok = account_meta.name and account_meta.email and account_meta.cui %}
|
||||||
<div id="sectiune-plan" style="margin-bottom:20px; padding-bottom:20px; border-bottom:1px solid var(--line);">
|
{% set _rar_ok = medii_disponibile and medii_disponibile|length > 0 %}
|
||||||
<h3 style="font-size:var(--fs-sm); color:var(--muted); font-weight:500; margin:0 0 10px;
|
<div class="card" id="card-cont" data-activ-subtab="{{ activ_subtab|default('firma') }}">
|
||||||
text-transform:uppercase; letter-spacing:.04em;">Plan curent</h3>
|
<div class="setari-head">
|
||||||
|
<h2 class="setari-title">
|
||||||
<div style="font-size:var(--fs-md); font-weight:600; margin-bottom:6px;
|
<svg class="ic" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="3"/><path d="M12 2v3M12 19v3M2 12h3M19 12h3M4.6 4.6l2.1 2.1M17.3 17.3l2.1 2.1M4.6 19.4l2.1-2.1M17.3 6.7l2.1-2.1"/></svg>
|
||||||
color:{% if plan_limita_atinsa|default(false) %}var(--err){% elif plan_warn|default(false) %}var(--warn){% else %}var(--ink){% endif %};">
|
Setari
|
||||||
{{ plan_linie }}
|
</h2>
|
||||||
|
<p class="setari-sub">Datele firmei, credentialele RAR, cheia API si contul tau.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if monthly_limit_val is defined and monthly_limit_val is not none and effective_tier_name|default('') == 'free' %}
|
<div class="setari-subtabs" role="tablist" aria-label="Sectiuni setari">
|
||||||
<div style="font-size:var(--fs-sm); color:var(--muted); margin-bottom:8px;">
|
<button type="button" class="setari-subtab{{ ' is-active' if activ_subtab|default('firma') == 'firma' else '' }}" role="tab" data-subtab="firma"
|
||||||
Planul Gratuit include {{ monthly_limit_val }} prestatii/luna prin dashboard-ul web.
|
aria-selected="{{ 'true' if activ_subtab|default('firma') == 'firma' else 'false' }}">
|
||||||
{% if plan_limita_atinsa|default(false) %}
|
<span class="st-dot {{ 'ok' if _firma_ok else 'warn' }}" aria-hidden="true"></span>Date firma</button>
|
||||||
Limita lunara a fost atinsa — trimiterile noi sunt blocate pana la inceputul lunii urmatoare.
|
<button type="button" class="setari-subtab{{ ' is-active' if activ_subtab|default('firma') == 'rar' else '' }}" role="tab" data-subtab="rar"
|
||||||
{% elif plan_warn|default(false) %}
|
aria-selected="{{ 'true' if activ_subtab|default('firma') == 'rar' else 'false' }}">
|
||||||
Te apropii de limita lunara.
|
<span class="st-dot {{ 'ok' if _rar_ok else 'warn' }}" aria-hidden="true"></span>Credentiale RAR</button>
|
||||||
{% endif %}
|
<button type="button" class="setari-subtab{{ ' is-active' if activ_subtab|default('firma') == 'api' else '' }}" role="tab" data-subtab="api"
|
||||||
|
aria-selected="{{ 'true' if activ_subtab|default('firma') == 'api' else 'false' }}">
|
||||||
|
<span class="st-dot {{ 'ok' if are_cheie else 'warn' }}" aria-hidden="true"></span>Cheie API</button>
|
||||||
|
<button type="button" class="setari-subtab{{ ' is-active' if activ_subtab|default('firma') == 'cont' else '' }}" role="tab" data-subtab="cont"
|
||||||
|
aria-selected="{{ 'true' if activ_subtab|default('firma') == 'cont' else 'false' }}">
|
||||||
|
<span class="st-dot info" aria-hidden="true"></span>Cont</button>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div style="font-size:var(--fs-sm); color:var(--muted); padding:8px 10px;
|
{# ============================ Sub-tab: Date firma ============================ #}
|
||||||
border:1px solid var(--line); border-radius:6px; margin-top:4px;">
|
<div class="setari-pane" data-subpane="firma" role="tabpanel"
|
||||||
Vrei sa treci pe Standard, Pro sau Premium?
|
{% if activ_subtab|default('firma') != 'firma' %}hidden{% endif %}>
|
||||||
Contacteaza-ne pentru alocare manuala — nu exista inca plata self-service.
|
<div class="setari-zone-title">
|
||||||
<strong>Pro</strong> adauga import prin API; <strong>Standard</strong> si
|
<svg class="ic" viewBox="0 0 24 24" aria-hidden="true"><path d="M4 21V4a1 1 0 0 1 1-1h9a1 1 0 0 1 1 1v17"/><path d="M15 9h4a1 1 0 0 1 1 1v11"/><path d="M2 21h20"/><path d="M8 7h3M8 11h3M8 15h3"/></svg>
|
||||||
<strong>Premium</strong> ridica limita de volum.
|
Date firma
|
||||||
|
<span class="setari-badge {{ 'sb-ok' if _firma_ok else 'sb-warn' }}">{{ 'complet' if _firma_ok else 'incomplet' }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<p class="setari-zone-desc">Apar pe declaratiile catre RAR si pe facturile de plan.</p>
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<!-- Sectiunea: Date firma (US-002) -->
|
{% if date_firma_mesaj %}<div class="flash" style="margin-bottom:12px;">{{ date_firma_mesaj }}</div>{% endif %}
|
||||||
<div style="margin-bottom:20px; padding-bottom:20px; border-bottom:1px solid var(--line);">
|
{% if date_firma_eroare %}<div class="banner" style="margin-bottom:12px; padding:8px 12px;">{{ date_firma_eroare }}</div>{% endif %}
|
||||||
<h3 style="font-size:13px; color:var(--muted); font-weight:500; margin:0 0 8px; text-transform:uppercase; letter-spacing:.04em;">Date firma</h3>
|
|
||||||
|
|
||||||
{% if date_firma_mesaj %}
|
<form hx-post="/cont/date-firma" hx-target="#card-cont" hx-swap="outerHTML">
|
||||||
<div class="flash" style="margin-bottom:12px;">{{ date_firma_mesaj }}</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if date_firma_eroare %}
|
|
||||||
<div class="banner" style="margin-bottom:12px; padding:8px 12px;">{{ date_firma_eroare }}</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<form hx-post="/cont/date-firma"
|
|
||||||
hx-target="#card-cont"
|
|
||||||
hx-swap="outerHTML">
|
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||||
<p style="margin:0 0 8px;">
|
<p style="margin:0 0 8px;">
|
||||||
<label style="font-size:13px; color:var(--muted);">Companie</label><br>
|
<label style="font-size:13px; color:var(--muted);">Companie</label><br>
|
||||||
<input type="text" name="companie" required
|
<input type="text" name="companie" required value="{{ account_meta.name or '' }}"
|
||||||
value="{{ account_meta.name or '' }}"
|
style="width:100%; max-width:340px;" placeholder="Numele firmei (ex. Service Auto SRL)">
|
||||||
style="width:100%; max-width:340px;"
|
|
||||||
placeholder="Numele firmei (ex. Service Auto SRL)">
|
|
||||||
</p>
|
</p>
|
||||||
<p style="margin:0 0 8px;">
|
<p style="margin:0 0 8px;">
|
||||||
<label style="font-size:13px; color:var(--muted);">Email contact</label><br>
|
<label style="font-size:13px; color:var(--muted);">Email contact</label><br>
|
||||||
<input type="email" name="email" required
|
<input type="email" name="email" required value="{{ account_meta.email or '' }}"
|
||||||
value="{{ account_meta.email or '' }}"
|
style="width:100%; max-width:340px;" placeholder="contact@firma.ro">
|
||||||
style="width:100%; max-width:340px;"
|
|
||||||
placeholder="contact@firma.ro">
|
|
||||||
</p>
|
</p>
|
||||||
<p style="margin:0 0 12px;">
|
<p style="margin:0 0 12px;">
|
||||||
<label style="font-size:13px; color:var(--muted);">CUI (cod unic de identificare)</label><br>
|
<label style="font-size:13px; color:var(--muted);">CUI (cod unic de identificare)</label><br>
|
||||||
<input type="text" name="cui" required
|
<input type="text" name="cui" required value="{{ account_meta.cui or '' }}"
|
||||||
value="{{ account_meta.cui or '' }}"
|
style="width:100%; max-width:340px;" placeholder="RO12345678">
|
||||||
style="width:100%; max-width:340px;"
|
|
||||||
placeholder="RO12345678">
|
|
||||||
</p>
|
</p>
|
||||||
<button type="submit">Salveaza datele firmei</button>
|
<button type="submit">Salveaza datele firmei</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Sectiunea: Cheia mea API -->
|
{# ========================= Sub-tab: Credentiale RAR ========================= #}
|
||||||
<div style="margin-bottom:20px; padding-bottom:20px; border-bottom:1px solid var(--line);">
|
<div class="setari-pane" data-subpane="rar" role="tabpanel"
|
||||||
<h3 style="font-size:13px; color:var(--muted); font-weight:500; margin:0 0 8px; text-transform:uppercase; letter-spacing:.04em;">Cheia mea API</h3>
|
{% if activ_subtab|default('firma') != 'rar' %}hidden{% endif %}>
|
||||||
|
<div class="setari-zone-title">
|
||||||
{% if api_key %}
|
<svg class="ic" viewBox="0 0 24 24" aria-hidden="true"><path d="M5 11l1.5-4A2 2 0 0 1 8.4 5.6h7.2A2 2 0 0 1 17.5 7L19 11"/><path d="M4 11h16v6H4z"/><path d="M6 17v2M18 17v2"/><circle cx="7.5" cy="14" r="1"/><circle cx="16.5" cy="14" r="1"/></svg>
|
||||||
<div class="flash" style="margin-bottom:12px;">Cheia a fost rotita. Salveaz-o acum — nu o vei mai putea vedea.</div>
|
Credentiale RAR (portal AUTOPASS)
|
||||||
|
<span class="setari-badge {{ 'sb-ok' if _rar_ok else 'sb-warn' }}">{{ 'configurat' if _rar_ok else 'neconfigurat' }}</span>
|
||||||
<div class="card" style="font-family:monospace; word-break:break-all; font-size:14px; background:#0f1115; margin:0 0 8px;">
|
|
||||||
{{ api_key }}
|
|
||||||
</div>
|
</div>
|
||||||
|
<p class="setari-zone-desc">
|
||||||
<button type="button"
|
Configureaza mediile RAR — Testare si/sau Productie — fiecare cu credentialele lui,
|
||||||
data-key="{{ api_key }}"
|
validate separat la salvare pe sistemul RAR corespunzator. Un mediu devine activ cand e
|
||||||
onclick="navigator.clipboard.writeText(this.dataset.key).then(()=>this.textContent='Copiat!')">
|
bifat si are credentiale; abia atunci importul web poate trimite pe el. (Canalul API ramane
|
||||||
Copiaza cheia
|
pe mediul global al instantei, {{ mediu_instanta() }}, pentru ca isi cara singur credentialele.)
|
||||||
</button>
|
|
||||||
|
|
||||||
<p style="font-size:13px; color:var(--warn); margin:10px 0 0;">
|
|
||||||
Atentie: la urmatoarea vizita aceasta cheie dispare. Daca o pierzi, roteste din nou.
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if rot_eroare %}
|
|
||||||
<div class="banner" style="margin-bottom:12px; padding:8px 12px;">{{ rot_eroare }}</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<form hx-post="/cont/roteste-cheie"
|
|
||||||
hx-target="#card-cont"
|
|
||||||
hx-swap="outerHTML"
|
|
||||||
style="margin-top:{% if api_key %}12px{% else %}0{% endif %};">
|
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
|
||||||
<button type="submit" style="background:var(--card); color:var(--warn); border-color:var(--warn);">
|
|
||||||
Roteste cheia API
|
|
||||||
</button>
|
|
||||||
<span style="font-size:12px; color:var(--muted); margin-left:8px;">Cheia veche se revoca imediat.</span>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Sectiunea: Credentiale RAR -->
|
|
||||||
<div>
|
|
||||||
<h3 style="font-size:13px; color:var(--muted); font-weight:500; margin:0 0 12px; text-transform:uppercase; letter-spacing:.04em;">Credentiale RAR (portal AUTOPASS)</h3>
|
|
||||||
|
|
||||||
<p class="muted" style="font-size:12px; margin:0 0 12px; line-height:1.5;">
|
|
||||||
Configureaza mai jos mediile RAR — Testare si/sau Productie — fiecare cu credentialele
|
|
||||||
lui, validate separat la salvare pe sistemul RAR corespunzator. Un mediu devine activ
|
|
||||||
cand e bifat si are credentiale; abia atunci importul web poate trimite pe el. Fara
|
|
||||||
niciun mediu activ, importul web nu are credentiale si trimiterile esueaza. (Canalul API
|
|
||||||
ramane pe mediul global al instantei, {{ mediu_instanta() }}, pentru ca isi cara singur
|
|
||||||
credentialele.)
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{% if creds_mesaj %}
|
{% if creds_mesaj %}<div class="flash" style="margin-bottom:12px;">{{ creds_mesaj }}</div>{% endif %}
|
||||||
<div class="flash" style="margin-bottom:12px;">{{ creds_mesaj }}</div>
|
{% if creds_eroare %}<div class="banner" style="margin-bottom:12px; padding:8px 12px;">{{ creds_eroare }}</div>{% endif %}
|
||||||
{% endif %}
|
|
||||||
{% if creds_eroare %}
|
|
||||||
<div class="banner" style="margin-bottom:12px; padding:8px 12px;">{{ creds_eroare }}</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<form hx-post="/cont/rar-medii"
|
<form hx-post="/cont/rar-medii" hx-target="#card-cont" hx-swap="outerHTML">
|
||||||
hx-target="#card-cont"
|
|
||||||
hx-swap="outerHTML">
|
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||||
|
|
||||||
<!-- Subsectiunea: Testare -->
|
<div class="env-cards">
|
||||||
<div style="margin-bottom:16px; padding-bottom:16px; border-bottom:1px solid var(--line);">
|
<!-- Testare -->
|
||||||
<p style="margin:0 0 6px;">
|
<div class="env-card{% if test_disponibil %} active-test{% endif %}">
|
||||||
<label style="font-size:13px; color:var(--muted); display:flex; align-items:center; gap:6px;">
|
<h4>Testare
|
||||||
|
{% if test_disponibil %}<span class="setari-badge sb-ok">configurat</span>{% else %}<span class="setari-badge sb-info">inactiv</span>{% endif %}
|
||||||
|
</h4>
|
||||||
|
<label class="env-sw">
|
||||||
<input type="checkbox" name="test_enabled" value="1" {% if test_enabled %}checked{% endif %}>
|
<input type="checkbox" name="test_enabled" value="1" {% if test_enabled %}checked{% endif %}>
|
||||||
Activare Testare
|
Activare Testare
|
||||||
</label>
|
</label>
|
||||||
{% if test_disponibil %}
|
<label class="env-lbl">Email RAR Testare</label>
|
||||||
<span style="font-size:12px; color:var(--ok);">configurat</span>
|
<input type="email" name="test_email" placeholder="email@service.ro">
|
||||||
{% endif %}
|
<label class="env-lbl">Parola RAR Testare</label>
|
||||||
</p>
|
<input type="password" name="test_parola" autocomplete="new-password">
|
||||||
<p style="margin:0 0 8px;">
|
{% if creds_test_mesaj %}<div class="flash" style="margin-top:6px;">{{ creds_test_mesaj }}</div>{% endif %}
|
||||||
<label style="font-size:13px; color:var(--muted);">Email RAR Testare</label><br>
|
{% if creds_test_eroare %}<div class="banner" style="margin-top:6px; padding:8px 12px;">{{ creds_test_eroare }}</div>{% endif %}
|
||||||
<input type="email" name="test_email" style="width:100%; max-width:340px;"
|
|
||||||
placeholder="email@service.ro">
|
|
||||||
</p>
|
|
||||||
<p style="margin:0 0 8px;">
|
|
||||||
<label style="font-size:13px; color:var(--muted);">Parola RAR Testare</label><br>
|
|
||||||
<input type="password" name="test_parola" style="width:100%; max-width:340px;"
|
|
||||||
autocomplete="new-password">
|
|
||||||
</p>
|
|
||||||
{% if creds_test_mesaj %}
|
|
||||||
<div class="flash" style="margin-top:6px;">{{ creds_test_mesaj }}</div>
|
|
||||||
{% endif %}
|
|
||||||
{% if creds_test_eroare %}
|
|
||||||
<div class="banner" style="margin-top:6px; padding:8px 12px;">{{ creds_test_eroare }}</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Subsectiunea: Productie -->
|
<!-- Productie -->
|
||||||
<div style="margin-bottom:16px; padding-bottom:16px; border-bottom:1px solid var(--line);">
|
<div class="env-card{% if not prod_disponibil %} inactive{% endif %}">
|
||||||
<p style="margin:0 0 6px;">
|
<h4>Productie
|
||||||
<label style="font-size:13px; color:var(--muted); display:flex; align-items:center; gap:6px;">
|
{% if prod_disponibil %}<span class="setari-badge sb-ok">configurat</span>{% else %}<span class="setari-badge sb-warn">inactiv</span>{% endif %}
|
||||||
|
</h4>
|
||||||
|
<label class="env-sw">
|
||||||
<input type="checkbox" name="prod_enabled" value="1" {% if prod_enabled %}checked{% endif %}>
|
<input type="checkbox" name="prod_enabled" value="1" {% if prod_enabled %}checked{% endif %}>
|
||||||
Activare Productie
|
Activare Productie
|
||||||
</label>
|
</label>
|
||||||
{% if prod_disponibil %}
|
<label class="env-lbl">Email RAR Productie</label>
|
||||||
<span style="font-size:12px; color:var(--ok);">configurat</span>
|
<input type="email" name="prod_email" placeholder="email@service.ro">
|
||||||
{% endif %}
|
<label class="env-lbl">Parola RAR Productie</label>
|
||||||
</p>
|
<input type="password" name="prod_parola" autocomplete="new-password">
|
||||||
<p style="margin:0 0 8px;">
|
<label class="env-confirm">
|
||||||
<label style="font-size:13px; color:var(--muted);">Email RAR Productie</label><br>
|
<input type="checkbox" name="prod_confirmare" value="1">
|
||||||
<input type="email" name="prod_email" style="width:100%; max-width:340px;"
|
<span>Inteleg ca trimiterile pe Productie sunt declaratii reale (L.142), finale si fara anulare.</span>
|
||||||
placeholder="email@service.ro">
|
|
||||||
</p>
|
|
||||||
<p style="margin:0 0 8px;">
|
|
||||||
<label style="font-size:13px; color:var(--muted);">Parola RAR Productie</label><br>
|
|
||||||
<input type="password" name="prod_parola" style="width:100%; max-width:340px;"
|
|
||||||
autocomplete="new-password">
|
|
||||||
</p>
|
|
||||||
<p style="margin:0 0 8px;">
|
|
||||||
<label style="font-size:13px; color:var(--muted); display:flex; align-items:flex-start; gap:6px;">
|
|
||||||
<input type="checkbox" name="prod_confirmare" value="1" style="margin-top:2px; flex-shrink:0;">
|
|
||||||
Inteleg ca trimiterile pe Productie sunt declaratii reale (L.142), finale si fara anulare.
|
|
||||||
</label>
|
</label>
|
||||||
</p>
|
{% if creds_prod_mesaj %}<div class="flash" style="margin-top:6px;">{{ creds_prod_mesaj }}</div>{% endif %}
|
||||||
{% if creds_prod_mesaj %}
|
{% if creds_prod_eroare %}<div class="banner" style="margin-top:6px; padding:8px 12px;">{{ creds_prod_eroare }}</div>{% endif %}
|
||||||
<div class="flash" style="margin-top:6px;">{{ creds_prod_mesaj }}</div>
|
</div>
|
||||||
{% endif %}
|
|
||||||
{% if creds_prod_eroare %}
|
|
||||||
<div class="banner" style="margin-top:6px; padding:8px 12px;">{{ creds_prod_eroare }}</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Selector mediu implicit -->
|
<div class="default-env">
|
||||||
<div style="margin-bottom:16px;">
|
<label style="font-size:13px; color:var(--muted);">Mediu implicit pentru trimiteri</label>
|
||||||
<label style="font-size:13px; color:var(--muted);">Mediu implicit pentru trimiteri</label><br>
|
|
||||||
{% if medii_disponibile %}
|
{% if medii_disponibile %}
|
||||||
<select name="rar_env_default" style="width:100%; max-width:340px; margin-top:4px;">
|
<select name="rar_env_default">
|
||||||
{% for env in medii_disponibile %}
|
{% for env in medii_disponibile %}
|
||||||
<option value="{{ env }}"{% if env == rar_env_default %} selected{% endif %}>{{ "Testare" if env == "test" else "Productie" }}</option>
|
<option value="{{ env }}"{% if env == rar_env_default %} selected{% endif %}>{{ "Testare" if env == "test" else "Productie" }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p style="font-size:13px; color:var(--muted); margin:4px 0 0;">Activeaza si valideaza un mediu intai.</p>
|
<span style="font-size:13px; color:var(--muted);">Activeaza si valideaza un mediu intai.</span>
|
||||||
{% endif %}
|
|
||||||
{% if creds_default_mesaj %}
|
|
||||||
<div class="flash" style="margin-top:6px;">{{ creds_default_mesaj }}</div>
|
|
||||||
{% endif %}
|
|
||||||
{% if creds_default_eroare %}
|
|
||||||
<div class="banner" style="margin-top:6px; padding:8px 12px;">{{ creds_default_eroare }}</div>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if creds_default_mesaj %}<div class="flash" style="margin-top:6px; flex-basis:100%;">{{ creds_default_mesaj }}</div>{% endif %}
|
||||||
|
{% if creds_default_eroare %}<div class="banner" style="margin-top:6px; padding:8px 12px; flex-basis:100%;">{{ creds_default_eroare }}</div>{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="submit">Salveaza mediile RAR</button>
|
<button type="submit">Salveaza mediile RAR</button>
|
||||||
<span style="font-size:12px; color:var(--muted); margin-left:8px;">Parolele stocate criptat, niciodata in clar.</span>
|
<span style="font-size:12px; color:var(--muted); margin-left:8px;">Parolele stocate criptat, niciodata in clar.</span>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{# ============================ Sub-tab: Cheie API ============================ #}
|
||||||
|
<div class="setari-pane" data-subpane="api" role="tabpanel"
|
||||||
|
{% if activ_subtab|default('firma') != 'api' %}hidden{% endif %}>
|
||||||
|
<div class="setari-zone-title">
|
||||||
|
<svg class="ic" viewBox="0 0 24 24" aria-hidden="true"><circle cx="8" cy="15" r="4"/><path d="M11 12l9-9"/><path d="M17 6l2.5 2.5M14 9l2.5 2.5"/></svg>
|
||||||
|
Cheie API
|
||||||
|
<span class="setari-badge {{ 'sb-ok' if are_cheie else 'sb-warn' }}">{{ 'activa' if are_cheie else 'lipsa' }}</span>
|
||||||
|
</div>
|
||||||
|
<p class="setari-zone-desc">
|
||||||
|
O foloseste softul propriu / ROAAUTO ca sa trimita prezentari prin API. Se vede integral o
|
||||||
|
singura data, la creare sau rotire.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{% if api_key %}
|
||||||
|
<div class="flash" style="margin-bottom:12px;">Cheia a fost rotita. Salveaz-o acum — nu o vei mai putea vedea.</div>
|
||||||
|
<div class="keybox">{{ api_key }}</div>
|
||||||
|
<button type="button" class="btn-secondary" style="margin-top:8px;"
|
||||||
|
data-key="{{ api_key }}"
|
||||||
|
onclick="navigator.clipboard.writeText(this.dataset.key).then(()=>this.textContent='Copiat!')">
|
||||||
|
Copiaza cheia
|
||||||
|
</button>
|
||||||
|
<p style="font-size:13px; color:var(--warn); margin:10px 0 0;">
|
||||||
|
Atentie: la urmatoarea vizita aceasta cheie dispare. Daca o pierzi, roteste din nou.
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if rot_eroare %}<div class="banner" style="margin:12px 0; padding:8px 12px;">{{ rot_eroare }}</div>{% endif %}
|
||||||
|
|
||||||
|
<form hx-post="/cont/roteste-cheie" hx-target="#card-cont" hx-swap="outerHTML"
|
||||||
|
style="margin-top:{% if api_key %}14px{% else %}0{% endif %};">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||||
|
<button type="submit" class="btn-warn">{{ 'Roteste cheia API' if are_cheie else 'Genereaza cheia API' }}</button>
|
||||||
|
<span style="font-size:12px; color:var(--muted); margin-left:8px;">Cheia veche se revoca imediat.</span>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# ========================= Sub-tab: Cont (plan + parola) ==================== #}
|
||||||
|
<div class="setari-pane" data-subpane="cont" role="tabpanel"
|
||||||
|
{% if activ_subtab|default('firma') != 'cont' %}hidden{% endif %}>
|
||||||
|
|
||||||
|
{# --- Plan curent --- #}
|
||||||
|
{% if plan_linie is defined %}
|
||||||
|
<div class="setari-zone-title">
|
||||||
|
<svg class="ic" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3 3 8l9 5 9-5z"/><path d="M3 13l9 5 9-5"/></svg>
|
||||||
|
Plan curent
|
||||||
|
<span class="setari-badge sb-info">{{ tier_label|default('Gratuit') }}</span>
|
||||||
|
</div>
|
||||||
|
<div style="font-size:var(--fs-md); font-weight:600; margin-bottom:6px;
|
||||||
|
color:{% if plan_limita_atinsa|default(false) %}var(--err){% elif plan_warn|default(false) %}var(--warn){% else %}var(--ink){% endif %};">
|
||||||
|
{{ plan_linie }}
|
||||||
|
</div>
|
||||||
|
{% if plan_limita_atinsa|default(false) %}
|
||||||
|
<p class="setari-zone-desc" style="color:var(--err);">Limita lunara a fost atinsa — trimiterile noi sunt blocate pana la inceputul lunii urmatoare.</p>
|
||||||
|
{% elif plan_warn|default(false) %}
|
||||||
|
<p class="setari-zone-desc" style="color:var(--warn);">Te apropii de limita lunara.</p>
|
||||||
|
{% endif %}
|
||||||
|
<p class="setari-zone-desc">
|
||||||
|
Pentru mai mult volum sau import prin API, <a href="mailto:contact@romfast.ro">contacteaza-ne</a> — alocarea planurilor se face manual.
|
||||||
|
</p>
|
||||||
|
<hr class="setari-subzone-sep">
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{# --- Parola de acces --- #}
|
||||||
|
<div class="setari-zone-title">
|
||||||
|
<svg class="ic" viewBox="0 0 24 24" aria-hidden="true"><rect x="4" y="10" width="16" height="11" rx="2"/><path d="M8 10V7a4 4 0 0 1 8 0v3"/><circle cx="12" cy="15.5" r="1.2"/></svg>
|
||||||
|
Parola de acces
|
||||||
|
</div>
|
||||||
|
<p class="setari-zone-desc">Parola cu care te autentifici in dashboard (separata de credentialele RAR). Minim 10 caractere.</p>
|
||||||
|
|
||||||
|
{% if parola_mesaj %}<div class="flash" style="margin-bottom:12px;">{{ parola_mesaj }}</div>{% endif %}
|
||||||
|
{% if parola_eroare %}<div class="banner" style="margin-bottom:12px; padding:8px 12px;">{{ parola_eroare }}</div>{% endif %}
|
||||||
|
|
||||||
|
<form hx-post="/cont/schimba-parola" hx-target="#card-cont" hx-swap="outerHTML">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||||
|
<p style="margin:0 0 8px;">
|
||||||
|
<label style="font-size:13px; color:var(--muted);">Parola actuala</label><br>
|
||||||
|
<input type="password" name="parola_actuala" autocomplete="current-password" style="width:100%; max-width:340px;">
|
||||||
|
</p>
|
||||||
|
<p style="margin:0 0 8px;">
|
||||||
|
<label style="font-size:13px; color:var(--muted);">Parola noua</label><br>
|
||||||
|
<input type="password" name="parola_noua" autocomplete="new-password" style="width:100%; max-width:340px;">
|
||||||
|
</p>
|
||||||
|
<p style="margin:0 0 12px;">
|
||||||
|
<label style="font-size:13px; color:var(--muted);">Confirma parola noua</label><br>
|
||||||
|
<input type="password" name="parola_confirma" autocomplete="new-password" style="width:100%; max-width:340px;">
|
||||||
|
</p>
|
||||||
|
<button type="submit">Schimba parola</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{# _editare_preview_modal.html — fragment de editare rand preview in modalul global.
|
{# _editare_preview_modal.html — fragment de editare rand preview in modalul global.
|
||||||
US-006 (PRD 5.12): swap-uit in #detaliu-modal-body de butonul Editeaza din preview.
|
Swap-uit in #detaliu-modal-body de butonul Editeaza din preview.
|
||||||
US-007 (PRD 5.12): butonul 'Confirma valorile' apare DOAR pe randurile needs_review
|
Butonul 'Confirma valorile' apare DOAR pe randurile needs_review si trimite CSRF
|
||||||
(T2): trimite CSRF POST la /confirma-review, inchide modalul via HX-Trigger-After-Settle.
|
POST la /confirma-review, inchide modalul via HX-Trigger-After-Settle.
|
||||||
|
|
||||||
Necesita din context:
|
Necesita din context:
|
||||||
import_id — id batch import
|
import_id — id batch import
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
{% if is_needs_review %}
|
{% if is_needs_review %}
|
||||||
{# T2 (US-007): Butonul 'Confirma valorile' apare DOAR pe randurile needs_review.
|
{# Butonul 'Confirma valorile' apare DOAR pe randurile needs_review.
|
||||||
POST separat (form propriu) la /confirma-review cu CSRF. Raspunsul inchide
|
POST separat (form propriu) la /confirma-review cu CSRF. Raspunsul inchide
|
||||||
modalul via HX-Trigger-After-Settle: inchideModal + swap OOB randul si countorii. #}
|
modalul via HX-Trigger-After-Settle: inchideModal + swap OOB randul si countorii. #}
|
||||||
<form hx-post="/_import/{{ import_id }}/rand/{{ row_index }}/confirma-review"
|
<form hx-post="/_import/{{ import_id }}/rand/{{ row_index }}/confirma-review"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{# _form_editare.html — partial partajat slim: campurile vehicul/data/odo + obs + chips prestatii.
|
{# _form_editare.html — partial partajat slim: campurile vehicul/data/odo + obs + chips prestatii.
|
||||||
US-007 (PRD 5.15): redesign slim cu VIN unic, Observatii textarea, chips prestatii (E4),
|
Redesign slim cu VIN unic, Observatii textarea, chips prestatii, si reveal dinamic
|
||||||
si reveal dinamic odometru initial cand chips contin R-ODO/I-ODO (D10c, E6 server-driven).
|
odometru initial cand chips contin R-ODO/I-ODO (server-driven).
|
||||||
|
|
||||||
Inclus cu {% include "_form_editare.html" %} INSIDE un <form> element al
|
Inclus cu {% include "_form_editare.html" %} INSIDE un <form> element al
|
||||||
template-ului parinte. Acel parinte pune form-ul, CSRF-ul si orice campuri
|
template-ului parinte. Acel parinte pune form-ul, CSRF-ul si orice campuri
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
err_map=err_map, fix_map=fix_map, vin_context=vin_context) }}
|
err_map=err_map, fix_map=fix_map, vin_context=vin_context) }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# === 3. Observatii (obs) — textarea liber, US-005 === #}
|
{# === 3. Observatii (obs) — textarea liber === #}
|
||||||
<div class="camp-slim">
|
<div class="camp-slim">
|
||||||
<label for="c-obs">Observatii (operatiile efectuate)</label>
|
<label for="c-obs">Observatii (operatiile efectuate)</label>
|
||||||
<textarea id="c-obs" name="obs" rows="2"
|
<textarea id="c-obs" name="obs" rows="2"
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
placeholder="ex: Revizie; schimbare placute frana">{{ obs_val or '' }}</textarea>
|
placeholder="ex: Revizie; schimbare placute frana">{{ obs_val or '' }}</textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# === 4. Prestatii chips (E4 server-driven, US-007) === #}
|
{# === 4. Prestatii chips (server-driven) === #}
|
||||||
{% set form_chips_url = form_chips_url or '/form-chips' %}
|
{% set form_chips_url = form_chips_url or '/form-chips' %}
|
||||||
{% set chips_section_id = 'chips-section' %}
|
{% set chips_section_id = 'chips-section' %}
|
||||||
{% include "_chips_prestatii.html" %}
|
{% include "_chips_prestatii.html" %}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
Lipseste o cheie API activa.
|
Lipseste o cheie API activa.
|
||||||
{% endif %}
|
{% endif %}
|
||||||
Mergi la <a href="/?tab=cont">Meniu cont › Cont</a> pentru a le configura.
|
Mergi la <a href="/?tab=cont">Meniu cont › Setari</a> pentru a le configura.
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<p class="muted" style="font-size:12px; margin:10px 0 0;">
|
<p class="muted" style="font-size:12px; margin:10px 0 0;">
|
||||||
Cererile trimit doar cheia API + datele prezentarii. Credentialele RAR se configureaza
|
Cererile trimit doar cheia API + datele prezentarii. Credentialele RAR se configureaza
|
||||||
o data in <a href="/?tab=cont">Cont</a> si sunt folosite automat la trimitere. Optional,
|
o data in <a href="/?tab=cont">Setari</a> si sunt folosite automat la trimitere. Optional,
|
||||||
poti include <code>rar_credentials</code> in payload ca sa le suprascrii pe acea cerere.
|
poti include <code>rar_credentials</code> in payload ca sa le suprascrii pe acea cerere.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -216,7 +216,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# Card: De ce nu ajunge la RAR? (PRD 5.19 US-010 — onestitate canal API) #}
|
{# Card: De ce nu ajunge la RAR? (onestitate canal API) #}
|
||||||
{# Un POST /v1/prezentari poate intoarce 200 dar randul sa NU plece la RAR. #}
|
{# Un POST /v1/prezentari poate intoarce 200 dar randul sa NU plece la RAR. #}
|
||||||
<div class="card" style="margin-bottom:16px;">
|
<div class="card" style="margin-bottom:16px;">
|
||||||
<h3 style="margin:0 0 6px; font-size:15px;">De ce nu ajunge la RAR?</h3>
|
<h3 style="margin:0 0 6px; font-size:15px;">De ce nu ajunge la RAR?</h3>
|
||||||
@@ -292,7 +292,7 @@
|
|||||||
<div id="integrare-test-rezultat" style="margin-top:8px;"></div>
|
<div id="integrare-test-rezultat" style="margin-top:8px;"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# Formular test credentiale RAR (US-007, PRD 5.20) #}
|
{# Formular test credentiale RAR #}
|
||||||
{# Login de proba pe mediul ales — fara efecte secundare, nu salveaza nimic. #}
|
{# Login de proba pe mediul ales — fara efecte secundare, nu salveaza nimic. #}
|
||||||
{# Banner-ul de rezultat include eticheta mediului ("pe TESTARE" / "pe PRODUCTIE"). #}
|
{# Banner-ul de rezultat include eticheta mediului ("pe TESTARE" / "pe PRODUCTIE"). #}
|
||||||
<div class="card" style="margin-bottom:16px;">
|
<div class="card" style="margin-bottom:16px;">
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
<span class="muted" style="font-size:12px;">doar evenimentele contului tau</span>
|
<span class="muted" style="font-size:12px;">doar evenimentele contului tau</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{# Badge mediu RAR activ (US-010 PRD 5.20) — mediul implicit al contului #}
|
{# Badge mediu RAR activ — mediul implicit al contului #}
|
||||||
{% if env_default | default('') %}
|
{% if env_default | default('') %}
|
||||||
{% set _eb = eticheta_env(env_default) %}
|
{% set _eb = eticheta_env(env_default) %}
|
||||||
<span class="{{ _eb[1] }}" style="margin-left:auto;" title="Mediu RAR activ">{{ _eb[0] }}</span>
|
<span class="{{ _eb[1] }}" style="margin-left:auto;" title="Mediu RAR activ">{{ _eb[0] }}</span>
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
{# Macro-uri partajate intre template-urile de import si mapari. #}
|
{# Macro-uri partajate intre template-urile de import si mapari. #}
|
||||||
|
|
||||||
{# US-002 (PRD 5.11): autosend_toggle neutralizat — auto_send nu mai tine randuri (US-001).
|
{# autosend_toggle neutralizat — auto_send nu mai tine randuri.
|
||||||
Simbolul pastrat (apelat in _mapari.html, _preview_import.html, _trimitere_detaliu.html)
|
Simbolul pastrat (apelat in _mapari.html, _preview_import.html, _trimitere_detaliu.html)
|
||||||
dar intoarce string gol. Coloanele DB raman (default 1, ne-citite pentru hold). #}
|
dar intoarce string gol. Coloanele DB raman (default 1, ne-citite pentru hold). #}
|
||||||
{% macro autosend_toggle(form_id='', checked=True, label='') -%}{%- endmacro %}
|
{% macro autosend_toggle(form_id='', checked=True, label='') -%}{%- endmacro %}
|
||||||
|
|
||||||
{# US-005 (PRD 5.12): macro `camp` partajat — extras din _trimitere_detaliu.html si
|
{# macro `camp` partajat — extras din _trimitere_detaliu.html si
|
||||||
_preview_rand.html. Suporta tip='date' (calendar nativ, D#10/R3) si tip='text' (default).
|
_preview_rand.html. Suporta tip='date' (calendar nativ) si tip='text' (default).
|
||||||
|
|
||||||
Parametri:
|
Parametri:
|
||||||
nome — name="" al input-ului (si cheie in err_map/fix_map)
|
nome — name="" al input-ului (si cheie in err_map/fix_map)
|
||||||
@@ -19,14 +19,14 @@
|
|||||||
id_prefix — prefix pentru id="" al input-ului (default 'c'; preview poate folosi 'e-N')
|
id_prefix — prefix pentru id="" al input-ului (default 'c'; preview poate folosi 'e-N')
|
||||||
#}
|
#}
|
||||||
{% macro camp(nome, eticheta, valoare, tip='text', err_map={}, fix_map={}, vin_context='', id_prefix='c', slim=False, mono=False) %}
|
{% macro camp(nome, eticheta, valoare, tip='text', err_map={}, fix_map={}, vin_context='', id_prefix='c', slim=False, mono=False) %}
|
||||||
{# slim=False: randare clasica (neschimbata). slim=True: varianta compacta (.camp-slim) din US-002 PRD 5.15:
|
{# slim=False: randare clasica (neschimbata). slim=True: varianta compacta (.camp-slim):
|
||||||
label 11px muted deasupra, input ~30px, fundal --card2.
|
label 11px muted deasupra, input ~30px, fundal --card2.
|
||||||
mono=True (valid numai cu slim=True): adauga clasa 'camp-mono' pe input pentru campuri
|
mono=True (valid numai cu slim=True): adauga clasa 'camp-mono' pe input pentru campuri
|
||||||
VIN/odometru/nr (IBM Plex Mono, prin .camp-slim .camp-mono din base.html). #}
|
VIN/odometru/nr (IBM Plex Mono, prin .camp-slim .camp-mono din base.html). #}
|
||||||
<div {% if slim %}class="camp-slim"{% else %}style="margin-bottom:10px;"{% endif %}>
|
<div {% if slim %}class="camp-slim"{% else %}style="margin-bottom:10px;"{% endif %}>
|
||||||
<label for="{{ id_prefix }}-{{ nome }}"{% if not slim %} class="muted" style="font-size:12px; display:block;"{% endif %}>{{ eticheta }}</label>
|
<label for="{{ id_prefix }}-{{ nome }}"{% if not slim %} class="muted" style="font-size:12px; display:block;"{% endif %}>{{ eticheta }}</label>
|
||||||
{% if tip == 'date' %}
|
{% if tip == 'date' %}
|
||||||
{# D#10/R3: degradare grijulie pentru valori ne-YYYY-MM-DD.
|
{# Degradare grijulie pentru valori ne-YYYY-MM-DD.
|
||||||
Daca valoarea nu e in formatul corect, inputul ramane gol + hint + hidden cu valoarea bruta
|
Daca valoarea nu e in formatul corect, inputul ramane gol + hint + hidden cu valoarea bruta
|
||||||
(ca sa nu se piarda tacut la submit). #}
|
(ca sa nu se piarda tacut la submit). #}
|
||||||
{%- set _dp_ok = (valoare and valoare|length == 10 and valoare[4:5] == '-' and valoare[7:8] == '-') -%}
|
{%- set _dp_ok = (valoare and valoare|length == 10 and valoare[4:5] == '-' and valoare[7:8] == '-') -%}
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{# PRD 5.13 — sistem butoane de actiune responsive.
|
{# Sistem butoane de actiune responsive.
|
||||||
CSS-ul aferent (.act, .act-tx, .act-ic, .act-save, .act-del, .act-group)
|
CSS-ul aferent (.act, .act-tx, .act-ic, .act-save, .act-del, .act-group)
|
||||||
este definit in base.html.
|
este definit in base.html.
|
||||||
Desktop: se afiseaza textul (.act-tx); mobil: se afiseaza iconita (.act-ic). #}
|
Desktop: se afiseaza textul (.act-tx); mobil: se afiseaza iconita (.act-ic). #}
|
||||||
|
|||||||
@@ -54,7 +54,7 @@
|
|||||||
<div class="muted">{{ e.denumire or '(fara denumire)' }}</div>
|
<div class="muted">{{ e.denumire or '(fara denumire)' }}</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="muted" style="font-size:12px;" data-eticheta="Sugestii">
|
<td class="muted" style="font-size:12px;" data-eticheta="Sugestii">
|
||||||
{# 5.18 US-007: badge sursa pe sugestia sistemului — confirmat (GOLD) / similar
|
{# Badge sursa pe sugestia sistemului — confirmat (GOLD) / similar
|
||||||
(SILVER+embedding k-NN) / non-operatie (pre-filtru NUL). Suggestion-only. #}
|
(SILVER+embedding k-NN) / non-operatie (pre-filtru NUL). Suggestion-only. #}
|
||||||
{% if e.sugestie_principala %}
|
{% if e.sugestie_principala %}
|
||||||
{% if e.sugestie_principala.sursa == 'gold_partajat' %}
|
{% if e.sugestie_principala.sursa == 'gold_partajat' %}
|
||||||
|
|||||||
@@ -82,7 +82,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# Tabel mapare: coloana din fisier | exemplu | camp RAR (mockup 5.16 / US-013) #}
|
{# Tabel mapare: coloana din fisier | exemplu | camp RAR #}
|
||||||
<div class="tablewrap" style="margin-bottom:16px;">
|
<div class="tablewrap" style="margin-bottom:16px;">
|
||||||
<table style="border-collapse:collapse; width:100%;">
|
<table style="border-collapse:collapse; width:100%;">
|
||||||
<thead>
|
<thead>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{#
|
{#
|
||||||
_modal_confirma_trimitere.html — modal REAL de confirmare tipata (PRD 5.19 D4/F4/F5).
|
_modal_confirma_trimitere.html — modal REAL de confirmare tipata.
|
||||||
|
|
||||||
`hx-confirm` nativ (doar OK/Cancel) NU e suficient pentru o actiune ireversibila care
|
`hx-confirm` nativ (doar OK/Cancel) NU e suficient pentru o actiune ireversibila care
|
||||||
trimite N prezentari catre RAR. Acest modal arata numarul + mediul tinta (Testare /
|
trimite N prezentari catre RAR. Acest modal arata numarul + mediul tinta (Testare /
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
Preview —
|
Preview —
|
||||||
<span class="muted" style="font-weight:400;">{{ filename or ("import #" ~ import_id) }}</span>
|
<span class="muted" style="font-weight:400;">{{ filename or ("import #" ~ import_id) }}</span>
|
||||||
</h2>
|
</h2>
|
||||||
{# Badge mediu RAR (US-009): vizibil intotdeauna in preview (claritate tinta) #}
|
{# Badge mediu RAR: vizibil intotdeauna in preview (claritate tinta) #}
|
||||||
{% if rar_env %}
|
{% if rar_env %}
|
||||||
<span class="pill" style="font-size:var(--fs-xs);
|
<span class="pill" style="font-size:var(--fs-xs);
|
||||||
{% if rar_env == 'prod' %}background:color-mix(in srgb,#B4452F 15%,var(--card)); border-color:#B4452F; color:#B4452F; font-weight:700;
|
{% if rar_env == 'prod' %}background:color-mix(in srgb,#B4452F 15%,var(--card)); border-color:#B4452F; color:#B4452F; font-weight:700;
|
||||||
@@ -71,7 +71,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Panou inline: operatii fara cod RAR, mapabile in flux (fara re-upload).
|
<!-- Panou inline: operatii fara cod RAR, mapabile in flux (fara re-upload).
|
||||||
US-004: un singur <form> cu un select per operatie + un singur buton Salveaza. -->
|
Un singur <form> cu un select per operatie + un singur buton Salveaza. -->
|
||||||
{% if unmapped_ops %}
|
{% if unmapped_ops %}
|
||||||
<div class="card" style="border-color:var(--err); background:color-mix(in srgb, var(--err) 12%, var(--card)); margin-bottom:14px;">
|
<div class="card" style="border-color:var(--err); background:color-mix(in srgb, var(--err) 12%, var(--card)); margin-bottom:14px;">
|
||||||
<h3 style="font-size:14px; margin:0 0 6px;">Operatii de mapat la cod RAR</h3>
|
<h3 style="font-size:14px; margin:0 0 6px;">Operatii de mapat la cod RAR</h3>
|
||||||
@@ -98,7 +98,7 @@
|
|||||||
{% if e.suggestions or e.sugestie_principala or (e.surse_sugestie and e.surse_sugestie.nul) %}
|
{% if e.suggestions or e.sugestie_principala or (e.surse_sugestie and e.surse_sugestie.nul) %}
|
||||||
<div class="muted" style="font-size:12px; margin-top:4px;">
|
<div class="muted" style="font-size:12px; margin-top:4px;">
|
||||||
sugestii:
|
sugestii:
|
||||||
{# 5.18 US-007: badge sursa pe sugestia sistemului — confirmat (GOLD) / similar
|
{# Badge sursa pe sugestia sistemului — confirmat (GOLD) / similar
|
||||||
(SILVER+embedding k-NN) / non-operatie (pre-filtru NUL). Suggestion-only. #}
|
(SILVER+embedding k-NN) / non-operatie (pre-filtru NUL). Suggestion-only. #}
|
||||||
{% if e.sugestie_principala %}
|
{% if e.sugestie_principala %}
|
||||||
{% if e.sugestie_principala.sursa == 'gold_partajat' %}
|
{% if e.sugestie_principala.sursa == 'gold_partajat' %}
|
||||||
@@ -134,7 +134,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Banner discoverability (T1, US-007): vizibil cand exista randuri needs_review.
|
<!-- Banner discoverability: vizibil cand exista randuri needs_review.
|
||||||
Explica operatorului ca randurile cu 'Verifica valori' nu pleaca la RAR
|
Explica operatorului ca randurile cu 'Verifica valori' nu pleaca la RAR
|
||||||
pana le deschide in modal si apasa 'Confirma valorile'. Dispare via OOB
|
pana le deschide in modal si apasa 'Confirma valorile'. Dispare via OOB
|
||||||
cand summary.needs_review == 0. -->
|
cand summary.needs_review == 0. -->
|
||||||
@@ -213,7 +213,7 @@
|
|||||||
hx-target="#import-section"
|
hx-target="#import-section"
|
||||||
hx-swap="outerHTML">
|
hx-swap="outerHTML">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token or '' }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token or '' }}">
|
||||||
{# Mediu RAR ales la upload — propagar la commit (US-009) #}
|
{# Mediu RAR ales la upload — propagar la commit #}
|
||||||
<input type="hidden" name="rar_env" value="{{ rar_env or '' }}">
|
<input type="hidden" name="rar_env" value="{{ rar_env or '' }}">
|
||||||
<div class="sticky-bar">
|
<div class="sticky-bar">
|
||||||
<div style="flex:1; min-width:280px;">
|
<div style="flex:1; min-width:280px;">
|
||||||
@@ -285,7 +285,7 @@ din <span id="n-hint-ok">{{ summary.get('ok', 0) }}</span> gata de trimis
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Actualizeaza N dupa editare/confirmare rand (OOB).
|
/* Actualizeaza N dupa editare/confirmare rand (OOB).
|
||||||
US-007: reviewed_rows (checkboxe) eliminate; N = randurile ok din DB,
|
reviewed_rows (checkboxe) eliminate; N = randurile ok din DB,
|
||||||
actualizate via OOB (#preview-ok-count[data-ok]) dupa /confirma-review sau /editeaza. */
|
actualizate via OOB (#preview-ok-count[data-ok]) dupa /confirma-review sau /editeaza. */
|
||||||
function updateN() {
|
function updateN() {
|
||||||
var total = getOk();
|
var total = getOk();
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{#
|
{#
|
||||||
_preview_rand.html — un singur rand de preview import.
|
_preview_rand.html — un singur rand de preview import.
|
||||||
US-006 (PRD 5.12): editarea inline (tr.preview-edit + mutual-exclusion script)
|
Editarea inline (tr.preview-edit + mutual-exclusion script) a fost eliminata.
|
||||||
a fost eliminata. Butonul Editeaza deschide MODALUL global (#detaliu-modal-body).
|
Butonul Editeaza deschide MODALUL global (#detaliu-modal-body).
|
||||||
|
|
||||||
Parametri:
|
Parametri:
|
||||||
editing — ELIMINAT (ignorat, pastrat pentru compatibilitate apeluri vechi)
|
editing — ELIMINAT (ignorat, pastrat pentru compatibilitate apeluri vechi)
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
<span class="pill {{ row.stare_css }}" style="display:inline-flex; align-items:center; gap:5px;"
|
<span class="pill {{ row.stare_css }}" style="display:inline-flex; align-items:center; gap:5px;"
|
||||||
{% if _nota %}title="{{ _nota }}"{% endif %}>
|
{% if _nota %}title="{{ _nota }}"{% endif %}>
|
||||||
<span aria-hidden="true" style="display:inline-block; width:7px; height:7px; border-radius:99px; background:currentColor; flex-shrink:0;"></span>{{ row.stare_eticheta }}</span>
|
<span aria-hidden="true" style="display:inline-block; width:7px; height:7px; border-radius:99px; background:currentColor; flex-shrink:0;"></span>{{ row.stare_eticheta }}</span>
|
||||||
{# Badge mediu RAR (US-010 PRD 5.20) — rar_env disponibil din contextul batch-ului. #}
|
{# Badge mediu RAR — rar_env disponibil din contextul batch-ului. #}
|
||||||
{% if rar_env | default('') %}
|
{% if rar_env | default('') %}
|
||||||
{% set _eb = eticheta_env(rar_env) %}
|
{% set _eb = eticheta_env(rar_env) %}
|
||||||
<div style="margin-top:3px;"><span class="{{ _eb[1] }}">{{ _eb[0] }}</span></div>
|
<div style="margin-top:3px;"><span class="{{ _eb[1] }}">{{ _eb[0] }}</span></div>
|
||||||
|
|||||||
@@ -14,10 +14,10 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{# === US-007 (PRD 5.19): banner coada tinuta imbatranita — mitigare OBLIGATORIE pt. default OFF.
|
{# Banner coada tinuta imbatranita — mitigare pt. default OFF.
|
||||||
Cand exista randuri `queued AND held=1` mai vechi de held_aged_days, semnalul de conformitate
|
Cand exista randuri `queued AND held=1` mai vechi de held_aged_days, semnalul de conformitate
|
||||||
L.142 (declarare obligatorie). Derivat (zero stare noua), varsta pe created_at.
|
L.142 (declarare obligatorie). Derivat (zero stare noua), varsta pe created_at.
|
||||||
Prioritate bannere (D7 — un-singur-banner "notice", ca sa nu impinga contoarele sub fold pe
|
Prioritate bannere (un-singur-banner "notice", ca sa nu impinga contoarele sub fold pe
|
||||||
mobil): cont inactiv (sus) + strip RAR jos raman separate; intre notice-uri, held-aged >
|
mobil): cont inactiv (sus) + strip RAR jos raman separate; intre notice-uri, held-aged >
|
||||||
trial (risc legal > notita trial) -> trial e suprimat cand held-aged e activ. #}
|
trial (risc legal > notita trial) -> trial e suprimat cand held-aged e activ. #}
|
||||||
{% set _held_aged_activ = (held_aged_count|default(0) > 0) and account_active %}
|
{% set _held_aged_activ = (held_aged_count|default(0) > 0) and account_active %}
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{# US-006 (5.17) — Banner one-time trial->Gratuit (T-DES-1): afisat la prima incarcare
|
{# Banner one-time trial->Gratuit: afisat la prima incarcare
|
||||||
dupa expirarea trial-ului. Discret, non-blocant; dismissibil via sessionStorage.
|
dupa expirarea trial-ului. Discret, non-blocant; dismissibil via sessionStorage.
|
||||||
Nu acopera stripul de sanatate (apare inainte de health strip, la acelasi nivel). #}
|
Nu acopera stripul de sanatate (apare inainte de health strip, la acelasi nivel). #}
|
||||||
{% if trial_expirat_recent|default(false) and not _held_aged_activ %}
|
{% if trial_expirat_recent|default(false) and not _held_aged_activ %}
|
||||||
@@ -60,7 +60,7 @@
|
|||||||
<script>(function(){ if(sessionStorage.getItem('tfx')){ var el=document.getElementById('banner-trial-expirat'); if(el) el.style.display='none'; } })();</script>
|
<script>(function(){ if(sessionStorage.getItem('tfx')){ var el=document.getElementById('banner-trial-expirat'); if(el) el.style.display='none'; } })();</script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{# === US-003 (PRD 5.16): Banda de stare RAR — NUMAI cand BLOCAT (rosu, lat de 100%).
|
{# === Banda de stare RAR — NUMAI cand BLOCAT (rosu, lat de 100%).
|
||||||
OK = dot verde in antet (base.html); banda nu mai apare cand totul e ok.
|
OK = dot verde in antet (base.html); banda nu mai apare cand totul e ok.
|
||||||
Elementul id="strip-sanatate" ramane in DOM mereu, dar goleste continutul cand OK,
|
Elementul id="strip-sanatate" ramane in DOM mereu, dar goleste continutul cand OK,
|
||||||
astfel "hidden" + fara continut eroare in sursa = nu pica testele de prezenta id-ului.
|
astfel "hidden" + fara continut eroare in sursa = nu pica testele de prezenta id-ului.
|
||||||
@@ -84,7 +84,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{# === US-002 (PRD 5.16): 5 carduri-contor separate (desktop) + bara compacta (mobil <=560px).
|
{# === 5 carduri-contor separate (desktop) + bara compacta (mobil <=560px).
|
||||||
Total / Luna asta / Azi / In coada / De corectat.
|
Total / Luna asta / Azi / In coada / De corectat.
|
||||||
#}
|
#}
|
||||||
{# Desktop: 5 carduri side-by-side. display:flex + layout stau in CSS (.contoare-desktop in
|
{# Desktop: 5 carduri side-by-side. display:flex + layout stau in CSS (.contoare-desktop in
|
||||||
@@ -109,7 +109,7 @@
|
|||||||
<div class="contor-label">Azi</div>
|
<div class="contor-label">Azi</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# In coada — contorul "In asteptare (manual)" se pliaza aici (US-006/D6): nu adaugam a 6-a
|
{# In coada — contorul "In asteptare (manual)" se pliaza aici: nu adaugam a 6-a
|
||||||
celula, ci o sub-linie amber cand exista randuri tinute (queued AND held=1). #}
|
celula, ci o sub-linie amber cand exista randuri tinute (queued AND held=1). #}
|
||||||
<div class="contor-card" style="flex:1; min-width:80px;">
|
<div class="contor-card" style="flex:1; min-width:80px;">
|
||||||
<div class="contor-cifra s-queued">{{ counts_queued }}</div>
|
<div class="contor-cifra s-queued">{{ counts_queued }}</div>
|
||||||
@@ -156,11 +156,11 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# === US-011 (5.20): Indicator mediu RAR activ per cont.
|
{# === Indicator mediu RAR activ per cont.
|
||||||
La 0 medii: nu afisa (cont fara configuratie RAR).
|
La 0 medii: nu afisa (cont fara configuratie RAR).
|
||||||
La 1 mediu: eticheta statica (fara toggle).
|
La 1 mediu: eticheta statica (fara toggle).
|
||||||
La 2 medii: buton de comutare HTMX (toggle conditionat).
|
La 2 medii: buton de comutare HTMX (toggle conditionat).
|
||||||
US-010: culori prin eticheta_env/clase .env-badge-* (consistenta cu badge-urile din liste).
|
Culori prin eticheta_env/clase .env-badge-* (consistenta cu badge-urile din liste).
|
||||||
#}
|
#}
|
||||||
{% set _medii = medii_disponibile | default([]) %}
|
{% set _medii = medii_disponibile | default([]) %}
|
||||||
{% set _env = env_default | default('prod') %}
|
{% set _env = env_default | default('prod') %}
|
||||||
@@ -194,7 +194,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{# === Navigatie rapida: Trimiteri + Mapari cu badge needs_mapping ===
|
{# === Navigatie rapida: Trimiteri + Mapari cu badge needs_mapping ===
|
||||||
Pastrata exact ca inainte (US-005): tab_activ determina marcajul activ.
|
Pastrata exact ca inainte: tab_activ determina marcajul activ.
|
||||||
#}
|
#}
|
||||||
{% set _tab = tab_activ | default('acasa') %}
|
{% set _tab = tab_activ | default('acasa') %}
|
||||||
<nav class="status-nav" aria-label="Navigatie rapida"
|
<nav class="status-nav" aria-label="Navigatie rapida"
|
||||||
@@ -207,7 +207,7 @@
|
|||||||
class="status-nav-link{% if _tab == 'mapari' %} status-nav-activ{% endif %}">Mapari{% if mapari_badge %}<span class="tab-badge" aria-hidden="true" style="display:inline-flex; align-items:center; justify-content:center; min-width:16px; height:16px; margin-left:4px; padding:0 4px; border-radius:99px; background:var(--err); color:#fff; font-size:11px; font-weight:700;">{{ mapari_badge }}</span>{% endif %}</a>
|
class="status-nav-link{% if _tab == 'mapari' %} status-nav-activ{% endif %}">Mapari{% if mapari_badge %}<span class="tab-badge" aria-hidden="true" style="display:inline-flex; align-items:center; justify-content:center; min-width:16px; height:16px; margin-left:4px; padding:0 4px; border-radius:99px; background:var(--err); color:#fff; font-size:11px; font-weight:700;">{{ mapari_badge }}</span>{% endif %}</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{# US-006 (5.17) + T-6 (5.16): linia de plan in CORP apare DOAR in starea de avertizare
|
{# Linia de plan in CORP apare DOAR in starea de avertizare
|
||||||
(>=80% -> --warn; limita atinsa -> --err). Consumul normal (N/60) traieste in badge-ul
|
(>=80% -> --warn; limita atinsa -> --err). Consumul normal (N/60) traieste in badge-ul
|
||||||
din antet + linia din meniul burger, nu ca rand permanent in corp (densitate redusa).
|
din antet + linia din meniul burger, nu ca rand permanent in corp (densitate redusa).
|
||||||
Ierarhie: nu concureaza cu stripul de sanatate (zero-silent-failures pastrat). #}
|
Ierarhie: nu concureaza cu stripul de sanatate (zero-silent-failures pastrat). #}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{#
|
{#
|
||||||
_stepper.html — Antet wizard import COMPACT (PUR vizual). PRD 5.13.
|
_stepper.html — Antet wizard import COMPACT (PUR vizual).
|
||||||
Parametru: `pas` (integer 1-4). Clasele .stepper-* sunt definite in base.html.
|
Parametru: `pas` (integer 1-4). Clasele .stepper-* sunt definite in base.html.
|
||||||
>=1024px: bara slim orizontala (.stepper-track). <1024px: forma colapsata
|
>=1024px: bara slim orizontala (.stepper-track). <1024px: forma colapsata
|
||||||
"Pasul N din 4 - Titlu" + bara de progres (.stepper-collapsed).
|
"Pasul N din 4 - Titlu" + bara de progres (.stepper-collapsed).
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<input type="hidden" id="f-page" name="page" value="{{ page | default(1) }}" hx-swap-oob="true">
|
<input type="hidden" id="f-page" name="page" value="{{ page | default(1) }}" hx-swap-oob="true">
|
||||||
|
|
||||||
{# OOB: sincronizeaza filtrul "doar tinute" (#f-held) cu starea randata acum, ca reincarcarile
|
{# OOB: sincronizeaza filtrul "doar tinute" (#f-held) cu starea randata acum, ca reincarcarile
|
||||||
(trimiteriChanged / paginare) sa-l pastreze in loc sa revina la lista completa (/code-review #2). #}
|
(trimiteriChanged / paginare) sa-l pastreze in loc sa revina la lista completa. #}
|
||||||
<input type="hidden" id="f-held" name="held" value="{{ f_held | default('') }}" hx-swap-oob="true">
|
<input type="hidden" id="f-held" name="held" value="{{ f_held | default('') }}" hx-swap-oob="true">
|
||||||
|
|
||||||
{# OOB: banner bulk "Trimite toate (N)" re-randat la fiecare reincarcare a listei (fix staleness). #}
|
{# OOB: banner bulk "Trimite toate (N)" re-randat la fiecare reincarcare a listei (fix staleness). #}
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
<span id="trimiteri-versiune" data-v="{{ versiune_trimiteri | default('') }}" hidden></span>
|
<span id="trimiteri-versiune" data-v="{{ versiune_trimiteri | default('') }}" hidden></span>
|
||||||
|
|
||||||
{% if bulk_message %}
|
{% if bulk_message %}
|
||||||
{# Sumar actiune bulk (US-010 PRD 5.15): afisat dupa bulk-fix, disparut la urmatoarea reincarcare. #}
|
{# Sumar actiune bulk: afisat dupa bulk-fix, disparut la urmatoarea reincarcare. #}
|
||||||
<div class="bulk-message" role="status" aria-live="polite"
|
<div class="bulk-message" role="status" aria-live="polite"
|
||||||
style="font-size:13px; color:var(--ink); background:var(--card2);
|
style="font-size:13px; color:var(--ink); background:var(--card2);
|
||||||
border:1px solid var(--line); border-radius:6px;
|
border:1px solid var(--line); border-radius:6px;
|
||||||
@@ -29,8 +29,8 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{# US-007 (5.19): chip filtru activ "doar tinute" cu buton de golire (fix /code-review #2 —
|
{# Chip filtru activ "doar tinute" cu buton de golire — filtrul held e sticky
|
||||||
filtrul held e sticky prin OOB, deci ii dam un off-switch explicit). #}
|
prin OOB, deci ii dam un off-switch explicit. #}
|
||||||
{% if f_held %}
|
{% if f_held %}
|
||||||
<div style="display:flex; align-items:center; gap:8px; margin-bottom:8px; font-size:var(--fs-sm);">
|
<div style="display:flex; align-items:center; gap:8px; margin-bottom:8px; font-size:var(--fs-sm);">
|
||||||
<span style="display:inline-flex; align-items:center; gap:6px; padding:2px 10px; border-radius:99px;
|
<span style="display:inline-flex; align-items:center; gap:6px; padding:2px 10px; border-radius:99px;
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if rows %}
|
{% if rows %}
|
||||||
{# Form bulk cu DOUA actiuni: (1) aplica cod RAR la selectate (bulk-fix, US-010),
|
{# Form bulk cu DOUA actiuni: (1) aplica cod RAR la selectate (bulk-fix),
|
||||||
(2) sterge selectate (sterge-bulk). Selectia opereaza DOAR pe randuri blocate
|
(2) sterge selectate (sterge-bulk). Selectia opereaza DOAR pe randuri blocate
|
||||||
(gestionabil); sent/sending/queued nu au checkbox (read-only).
|
(gestionabil); sent/sending/queued nu au checkbox (read-only).
|
||||||
Butonul "Aplica cod" foloseste hx-post propriu (override form action).
|
Butonul "Aplica cod" foloseste hx-post propriu (override form action).
|
||||||
@@ -62,7 +62,7 @@
|
|||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||||
<div style="display:flex; justify-content:flex-end; align-items:center;
|
<div style="display:flex; justify-content:flex-end; align-items:center;
|
||||||
gap:6px; margin-bottom:8px; flex-wrap:wrap;">
|
gap:6px; margin-bottom:8px; flex-wrap:wrap;">
|
||||||
{# Bulk-fix: input cod + buton aplica (US-010 PRD 5.15) #}
|
{# Bulk-fix: input cod + buton aplica #}
|
||||||
<input type="text" name="cod_prestatie" id="bulk-fix-cod"
|
<input type="text" name="cod_prestatie" id="bulk-fix-cod"
|
||||||
placeholder="Cod RAR (ex: OE-1)"
|
placeholder="Cod RAR (ex: OE-1)"
|
||||||
autocomplete="off" autocapitalize="characters"
|
autocomplete="off" autocapitalize="characters"
|
||||||
@@ -89,7 +89,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# Lista slim trimiteri (US-004, PRD 5.15).
|
{# Lista slim trimiteri.
|
||||||
Inlocuieste tabelul cu randuri compacte: VIN mono + operatie·ora + pill.
|
Inlocuieste tabelul cu randuri compacte: VIN mono + operatie·ora + pill.
|
||||||
Nr. inmatriculare, data prestatie si nr. prezentare RAR raman accesibile
|
Nr. inmatriculare, data prestatie si nr. prezentare RAR raman accesibile
|
||||||
pe linia meta discreta (linia 3) si in modalul de detaliu. #}
|
pe linia meta discreta (linia 3) si in modalul de detaliu. #}
|
||||||
@@ -160,14 +160,14 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# Zona dreapta: pill stare + badge mediu RAR (US-010 PRD 5.20) + buton Trimite (US-005) #}
|
{# Zona dreapta: pill stare + badge mediu RAR + buton Trimite #}
|
||||||
<div style="flex:0 0 auto; display:flex; flex-direction:column; align-items:flex-end; gap:3px; white-space:nowrap;">
|
<div style="flex:0 0 auto; display:flex; flex-direction:column; align-items:flex-end; gap:3px; white-space:nowrap;">
|
||||||
<span class="pill {{ r.stare_css }}" title="{{ r.stare_text }}">{{ r.stare_scurt }}</span>
|
<span class="pill {{ r.stare_css }}" title="{{ r.stare_text }}">{{ r.stare_scurt }}</span>
|
||||||
{% if r.rar_env %}
|
{% if r.rar_env %}
|
||||||
{% set _eb = eticheta_env(r.rar_env) %}
|
{% set _eb = eticheta_env(r.rar_env) %}
|
||||||
<span class="{{ _eb[1] }}">{{ _eb[0] }}</span>
|
<span class="{{ _eb[1] }}">{{ _eb[0] }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{# US-005 (PRD 5.19 / D6): buton "Trimite" DOAR pe randurile tinute (queued AND held=1).
|
{# Buton "Trimite" DOAR pe randurile tinute (queued AND held=1).
|
||||||
Afordanta dedicata cu stopPropagation (randul e role=button) — NU declanseaza modalul
|
Afordanta dedicata cu stopPropagation (randul e role=button) — NU declanseaza modalul
|
||||||
de detaliu. Confirmare 1-linie prin hx-confirm (per rand); ireversibilitate in microcopy. #}
|
de detaliu. Confirmare 1-linie prin hx-confirm (per rand); ireversibilitate in microcopy. #}
|
||||||
{% if r.tinut %}
|
{% if r.tinut %}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
{% set cod_afis = prez.cod_rar if (prez.cod_rar and prez.cod_rar != '—') else 'nemapat' %}
|
{% set cod_afis = prez.cod_rar if (prez.cod_rar and prez.cod_rar != '—') else 'nemapat' %}
|
||||||
<div class="card" id="detaliu-card-{{ id }}" style="border:none; padding:0; margin:0;">
|
<div class="card" id="detaliu-card-{{ id }}" style="border:none; padding:0; margin:0;">
|
||||||
|
|
||||||
{# === Header — #id + pill stare + badge mediu RAR (US-010 PRD 5.20) + motiv === #}
|
{# === Header — #id + pill stare + badge mediu RAR + motiv === #}
|
||||||
<div style="display:flex; align-items:center; gap:8px; flex-wrap:wrap; margin:0 0 8px;">
|
<div style="display:flex; align-items:center; gap:8px; flex-wrap:wrap; margin:0 0 8px;">
|
||||||
<h2 id="detaliu-modal-titlu" style="font-size:15px; margin:0;">Detaliu trimitere #{{ id }}</h2>
|
<h2 id="detaliu-modal-titlu" style="font-size:15px; margin:0;">Detaliu trimitere #{{ id }}</h2>
|
||||||
<span class="pill {{ stare_css }}">{{ stare_text }}</span>
|
<span class="pill {{ stare_css }}">{{ stare_text }}</span>
|
||||||
@@ -110,14 +110,13 @@
|
|||||||
hx-disabled-elt="find button">
|
hx-disabled-elt="find button">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||||
|
|
||||||
{# Cleanup B (US-009 PRD 5.15): vechiul <select name="cod_prestatie"> eliminat.
|
{# Vechiul <select name="cod_prestatie"> eliminat. Chips din _form_editare.html
|
||||||
Chips din _form_editare.html (via _chips_prestatii.html) il inlocuiesc complet:
|
(via _chips_prestatii.html) il inlocuiesc complet: emit hidden inputs
|
||||||
emit hidden inputs name="cod_prestatie" + picker per-operatie (E4, US-007).
|
name="cod_prestatie" + picker per-operatie.
|
||||||
post_corectie_trimitere foloseste form.getlist("cod_prestatie") → compatibil. #}
|
post_corectie_trimitere foloseste form.getlist("cod_prestatie") → compatibil. #}
|
||||||
|
|
||||||
{# Operatie service (cod intern + denumire venita prin API/import), distinct de
|
{# Operatie service (cod intern + denumire venita prin API/import), distinct de
|
||||||
operatia RAR mapata. op_service_cod="" cand lipseste → randul absent.
|
operatia RAR mapata. op_service_cod="" cand lipseste → randul absent. #}
|
||||||
RAMANE in _trimitere_detaliu.html (D#5). #}
|
|
||||||
{% if prez.op_service_cod %}
|
{% if prez.op_service_cod %}
|
||||||
<div style="margin:0 0 12px;">
|
<div style="margin:0 0 12px;">
|
||||||
<div class="muted" style="font-size:12px;">Operatie service</div>
|
<div class="muted" style="font-size:12px;">Operatie service</div>
|
||||||
@@ -125,7 +124,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{# === Campurile vehicul/data/odo + erori/fix + buton — partial DRY (US-005). === #}
|
{# === Campurile vehicul/data/odo + erori/fix + buton — partial DRY. === #}
|
||||||
{% include "_form_editare.html" %}
|
{% include "_form_editare.html" %}
|
||||||
</form>
|
</form>
|
||||||
{% else %}
|
{% else %}
|
||||||
@@ -150,7 +149,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{# === Rand tinut manual (queued AND held=1) — afordanta de eliberare (US-005/US-010, 5.19).
|
{# === Rand tinut manual (queued AND held=1) — afordanta de eliberare.
|
||||||
Buton dedicat „Trimite acum": elibereaza randul (held 1->0); worker-ul il preia la
|
Buton dedicat „Trimite acum": elibereaza randul (held 1->0); worker-ul il preia la
|
||||||
urmatorul poll. Reincarca lista + inchide modalul. Confirmare 1-linie (ireversibil). === #}
|
urmatorul poll. Reincarca lista + inchide modalul. Confirmare 1-linie (ireversibil). === #}
|
||||||
{% if tinut %}
|
{% if tinut %}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
hx-indicator="#upload-spinner">
|
hx-indicator="#upload-spinner">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token or '' }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token or '' }}">
|
||||||
|
|
||||||
{# Indicator mediu RAR (US-009, PRD 5.20): vizibil inainte de drop-zone #}
|
{# Indicator mediu RAR: vizibil inainte de drop-zone #}
|
||||||
{% set medii_rar = medii | default([]) %}
|
{% set medii_rar = medii | default([]) %}
|
||||||
{% if medii_rar | length == 0 %}
|
{% if medii_rar | length == 0 %}
|
||||||
{# Banner avertisment — non-blocant (upload continua; commit foloseste ancora globala) #}
|
{# Banner avertisment — non-blocant (upload continua; commit foloseste ancora globala) #}
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
border:1px solid var(--warn, #e6b34a); font-size:13px;" role="note">
|
border:1px solid var(--warn, #e6b34a); font-size:13px;" role="note">
|
||||||
<strong>Niciun mediu RAR activ.</strong>
|
<strong>Niciun mediu RAR activ.</strong>
|
||||||
Activeaza mediul de Productie sau Testare in
|
Activeaza mediul de Productie sau Testare in
|
||||||
<a href="?tab=cont" style="color:var(--accent);">Meniu cont › Cont</a>.
|
<a href="?tab=cont" style="color:var(--accent);">Meniu cont › Setari</a>.
|
||||||
</div>
|
</div>
|
||||||
{% elif medii_rar | length == 1 %}
|
{% elif medii_rar | length == 1 %}
|
||||||
{# Eticheta statica (un singur mediu disponibil) #}
|
{# Eticheta statica (un singur mediu disponibil) #}
|
||||||
|
|||||||
@@ -36,8 +36,8 @@
|
|||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
/* US-001 PRD 5.16: stive de font standard web. Toate regulile font-face IBM Plex sterse.
|
/* Stive de font standard web. Toate regulile font-face IBM Plex sterse.
|
||||||
Motiv: decizie user (risc AI-Slop #11 acceptat constient), uniformitate cross-page.
|
Motiv: decizie user, uniformitate cross-page.
|
||||||
Fisierele woff2 raman pe disc (curatare = follow-up optional, non-blocant).
|
Fisierele woff2 raman pe disc (curatare = follow-up optional, non-blocant).
|
||||||
Referinte catre directorul de fonturi statice eliminate — font-ui si font-mono sunt stive sistem. */
|
Referinte catre directorul de fonturi statice eliminate — font-ui si font-mono sunt stive sistem. */
|
||||||
/* Paleta dark (default) — accent azur ROMFAST.
|
/* Paleta dark (default) — accent azur ROMFAST.
|
||||||
@@ -45,10 +45,10 @@
|
|||||||
--line2: separator subtire (intre --bg si --line). */
|
--line2: separator subtire (intre --bg si --line). */
|
||||||
:root { --bg:#0f1218; --card:#181c24; --card2:#0f1218; --ink:#e6e9ef; --muted:#8b93a7; --line:#262b36; --line2:#1f2530;
|
:root { --bg:#0f1218; --card:#181c24; --card2:#0f1218; --ink:#e6e9ef; --muted:#8b93a7; --line:#262b36; --line2:#1f2530;
|
||||||
--ok:#2FBF8F; --warn:#E0A93B; --err:#E05D5D; --accent:#2E74D6;
|
--ok:#2FBF8F; --warn:#E0A93B; --err:#E05D5D; --accent:#2E74D6;
|
||||||
/* US-001 (PRD 5.16): stive font standard web — sursa unica de adevar */
|
/* Stive font standard web — sursa unica de adevar */
|
||||||
--font-ui: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
--font-ui: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||||
--font-mono: ui-monospace, "SF Mono", "Cascadia Code", "Segoe UI Mono", "Roboto Mono", Menlo, Consolas, monospace;
|
--font-mono: ui-monospace, "SF Mono", "Cascadia Code", "Segoe UI Mono", "Roboto Mono", Menlo, Consolas, monospace;
|
||||||
/* US-002 (PRD 5.16): scala tipografica uniforma — sursa unica de adevar */
|
/* Scala tipografica uniforma — sursa unica de adevar */
|
||||||
--fs-xs:12px; --fs-sm:13.5px; --fs-base:15px; --fs-md:16px;
|
--fs-xs:12px; --fs-sm:13.5px; --fs-base:15px; --fs-md:16px;
|
||||||
--fs-lg:18px; --fs-xl:20px; --fs-2xl:28px; --fs-3xl:34px;
|
--fs-lg:18px; --fs-xl:20px; --fs-2xl:28px; --fs-3xl:34px;
|
||||||
--lh-tight:1.25; --lh-body:1.55; }
|
--lh-tight:1.25; --lh-body:1.55; }
|
||||||
@@ -60,7 +60,7 @@
|
|||||||
[data-theme="petrol"] { --bg:#0e1416; --card:#161e20; --card2:#0e1416; --ink:#e6e9ef; --muted:#8b93a7; --line:#232c2e; --line2:#1c2426;
|
[data-theme="petrol"] { --bg:#0e1416; --card:#161e20; --card2:#0e1416; --ink:#e6e9ef; --muted:#8b93a7; --line:#232c2e; --line2:#1c2426;
|
||||||
--ok:#2FBF8F; --warn:#E0A93B; --err:#E05D5D; --accent:#0E7C7B; }
|
--ok:#2FBF8F; --warn:#E0A93B; --err:#E05D5D; --accent:#0E7C7B; }
|
||||||
/* Paleta Grafit — similara cu dark, accent azur deschis (#6ea2ec = landing --infot).
|
/* Paleta Grafit — similara cu dark, accent azur deschis (#6ea2ec = landing --infot).
|
||||||
Distinta de dark la cererea userului (D2 PRD 5.15). */
|
Distinta de dark la cererea userului. */
|
||||||
[data-theme="grafit"] { --bg:#0f1218; --card:#181c24; --card2:#0f1218; --ink:#e6e9ef; --muted:#8b93a7; --line:#262b36; --line2:#1f2530;
|
[data-theme="grafit"] { --bg:#0f1218; --card:#181c24; --card2:#0f1218; --ink:#e6e9ef; --muted:#8b93a7; --line:#262b36; --line2:#1f2530;
|
||||||
--ok:#2FBF8F; --warn:#E0A93B; --err:#E05D5D; --accent:#6ea2ec; }
|
--ok:#2FBF8F; --warn:#E0A93B; --err:#E05D5D; --accent:#6ea2ec; }
|
||||||
/* Paleta Cobalt — fundal bleumarin adanc, accent albastru viu (#8aa0ff = landing --infot). */
|
/* Paleta Cobalt — fundal bleumarin adanc, accent albastru viu (#8aa0ff = landing --infot). */
|
||||||
@@ -70,7 +70,7 @@
|
|||||||
[data-theme="cupru"] { --bg:#15110b; --card:#211a12; --card2:#15110b; --ink:#efe6d6; --muted:#a89a85; --line:#36291c; --line2:#281e14;
|
[data-theme="cupru"] { --bg:#15110b; --card:#211a12; --card2:#15110b; --ink:#efe6d6; --muted:#a89a85; --line:#36291c; --line2:#281e14;
|
||||||
--ok:#67b98c; --warn:#c97d2e; --err:#e2685a; --accent:#dfa45c; }
|
--ok:#67b98c; --warn:#c97d2e; --err:#e2685a; --accent:#dfa45c; }
|
||||||
/* Paleta Hartie — fundal crem cald, accent albastru clasic (#1F5FBF = landing --infot = --accent).
|
/* Paleta Hartie — fundal crem cald, accent albastru clasic (#1F5FBF = landing --infot = --accent).
|
||||||
Similara cu light, distinta la cererea userului (D2 PRD 5.15). */
|
Similara cu light, distinta la cererea userului. */
|
||||||
[data-theme="hartie"] { --bg:#f3efe6; --card:#fffdf7; --card2:#f3efe6; --ink:#1e1a13; --muted:#6a6052; --line:#e2dccc; --line2:#ece6d9;
|
[data-theme="hartie"] { --bg:#f3efe6; --card:#fffdf7; --card2:#f3efe6; --ink:#1e1a13; --muted:#6a6052; --line:#e2dccc; --line2:#ece6d9;
|
||||||
--ok:#1c7d5d; --warn:#b45309; --err:#bd463c; --accent:#1F5FBF; }
|
--ok:#1c7d5d; --warn:#b45309; --err:#bd463c; --accent:#1F5FBF; }
|
||||||
* { box-sizing:border-box; }
|
* { box-sizing:border-box; }
|
||||||
@@ -104,7 +104,7 @@
|
|||||||
th { color:var(--muted); font-weight:500; font-size:12px; text-transform:uppercase; letter-spacing:.04em; }
|
th { color:var(--muted); font-weight:500; font-size:12px; text-transform:uppercase; letter-spacing:.04em; }
|
||||||
.empty { color:var(--muted); padding:24px; text-align:center; }
|
.empty { color:var(--muted); padding:24px; text-align:center; }
|
||||||
.pill { font-size:12px; padding:2px 8px; border-radius:99px; border:1px solid var(--line); }
|
.pill { font-size:12px; padding:2px 8px; border-radius:99px; border:1px solid var(--line); }
|
||||||
/* Badge sursa sugestie (5.18 US-007): de unde vine sugestia de cod in editorul de mapare.
|
/* Badge sursa sugestie: de unde vine sugestia de cod in editorul de mapare.
|
||||||
confirmat = GOLD validat de om (verde); similar = SILVER/embedding k-NN (azur);
|
confirmat = GOLD validat de om (verde); similar = SILVER/embedding k-NN (azur);
|
||||||
non-operatie = pre-filtru NUL / vecin NUL (gri-cald). Suggestion-only, doar indiciu vizual. */
|
non-operatie = pre-filtru NUL / vecin NUL (gri-cald). Suggestion-only, doar indiciu vizual. */
|
||||||
.sugg-sursa { display:inline-block; font-size:10px; font-weight:700; line-height:1; padding:2px 6px;
|
.sugg-sursa { display:inline-block; font-size:10px; font-weight:700; line-height:1; padding:2px 6px;
|
||||||
@@ -142,7 +142,7 @@
|
|||||||
.s-ok{color:var(--ok);}
|
.s-ok{color:var(--ok);}
|
||||||
.s-needs_review{color:var(--warn);}
|
.s-needs_review{color:var(--warn);}
|
||||||
.s-already_sent,.s-duplicate_in_file{color:var(--muted);}
|
.s-already_sent,.s-duplicate_in_file{color:var(--muted);}
|
||||||
/* Badge mediu RAR (US-010 PRD 5.20) — semantica risc L.142.
|
/* Badge mediu RAR — semantica risc L.142.
|
||||||
Productie: fill atentie (ton --err), text alb — declaratie reala, ireversibila.
|
Productie: fill atentie (ton --err), text alb — declaratie reala, ireversibila.
|
||||||
Testare: outline discret, ton --muted — mediu de proba, low-stakes.
|
Testare: outline discret, ton --muted — mediu de proba, low-stakes.
|
||||||
NU se foloseste verde pentru Productie (ar semnala "sigur"). */
|
NU se foloseste verde pentru Productie (ar semnala "sigur"). */
|
||||||
@@ -167,7 +167,7 @@
|
|||||||
flex-wrap:wrap; z-index:10; }
|
flex-wrap:wrap; z-index:10; }
|
||||||
/* Indicator HTMX — ascuns pana la request */
|
/* Indicator HTMX — ascuns pana la request */
|
||||||
.htmx-indicator { display:none; }
|
.htmx-indicator { display:none; }
|
||||||
/* US-011 (PRD 5.16): selector tema stil pill — icon + eticheta temei curente.
|
/* Selector tema stil pill — icon + eticheta temei curente.
|
||||||
Eticheta se ascunde pe <=560px (spatiu ingust), ramane iconita. */
|
Eticheta se ascunde pe <=560px (spatiu ingust), ramane iconita. */
|
||||||
.tema-btn { display:inline-flex; align-items:center; gap:6px; height:36px; padding:0 12px;
|
.tema-btn { display:inline-flex; align-items:center; gap:6px; height:36px; padding:0 12px;
|
||||||
border-radius:8px; background:transparent; border:1px solid var(--line);
|
border-radius:8px; background:transparent; border:1px solid var(--line);
|
||||||
@@ -176,7 +176,7 @@
|
|||||||
.tema-btn:hover { border-color:var(--accent); color:var(--ink); }
|
.tema-btn:hover { border-color:var(--accent); color:var(--ink); }
|
||||||
.tema-btn:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
|
.tema-btn:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
|
||||||
@media (max-width:560px) { #tema-label { display:none; } }
|
@media (max-width:560px) { #tema-label { display:none; } }
|
||||||
/* US-003 (PRD 5.16): dot RAR compact in antet.
|
/* Dot RAR compact in antet.
|
||||||
Stare OK: dot verde pulsant + "RAR online". Stare BLOCAT: dot rosu.
|
Stare OK: dot verde pulsant + "RAR online". Stare BLOCAT: dot rosu.
|
||||||
Stilat ca pill; sensul NU depinde de culoare (aria-label + title). */
|
Stilat ca pill; sensul NU depinde de culoare (aria-label + title). */
|
||||||
.rar-chip { display:inline-flex; align-items:center; gap:7px; height:36px; padding:0 12px;
|
.rar-chip { display:inline-flex; align-items:center; gap:7px; height:36px; padding:0 12px;
|
||||||
@@ -190,7 +190,7 @@
|
|||||||
.rar-dot.live { animation:rar-pulse 2s ease-in-out infinite; }
|
.rar-dot.live { animation:rar-pulse 2s ease-in-out infinite; }
|
||||||
@keyframes rar-pulse { 0%,100%{opacity:1;} 50%{opacity:.5;} }
|
@keyframes rar-pulse { 0%,100%{opacity:1;} 50%{opacity:.5;} }
|
||||||
@media (max-width:560px) { .rar-chip .rar-tx { display:none; } }
|
@media (max-width:560px) { .rar-chip .rar-tx { display:none; } }
|
||||||
/* US-010 (PRD 5.16): sub-titlu cu numele service-ului (cand autentificat). */
|
/* Sub-titlu cu numele service-ului (cand autentificat). */
|
||||||
.h-sub { font-size:var(--fs-xs); color:var(--muted); margin-top:2px; line-height:1.2; }
|
.h-sub { font-size:var(--fs-xs); color:var(--muted); margin-top:2px; line-height:1.2; }
|
||||||
.h-sub .svc { color:var(--ink); font-weight:600; }
|
.h-sub .svc { color:var(--ink); font-weight:600; }
|
||||||
/* Badge env (test/prod) si badge tier (plan cont) langa titlu. */
|
/* Badge env (test/prod) si badge tier (plan cont) langa titlu. */
|
||||||
@@ -367,7 +367,7 @@
|
|||||||
border-radius:6px; cursor:pointer; min-height:36px; white-space:nowrap; }
|
border-radius:6px; cursor:pointer; min-height:36px; white-space:nowrap; }
|
||||||
.kebab-menu button:hover, .kebab-menu a:hover { background:var(--line); }
|
.kebab-menu button:hover, .kebab-menu a:hover { background:var(--line); }
|
||||||
.kebab-menu button.danger { color:var(--err); }
|
.kebab-menu button.danger { color:var(--err); }
|
||||||
/* === Accordion import compact (US-006 — regiune CSS disjuncta) ===
|
/* === Accordion import compact (regiune CSS disjuncta) ===
|
||||||
<details id="import-details"> invelete stepper + upload pe Acasa.
|
<details id="import-details"> invelete stepper + upload pe Acasa.
|
||||||
Atribut `open` setat de server din `are_trimiteri`:
|
Atribut `open` setat de server din `are_trimiteri`:
|
||||||
False (first-run) → open; True (returning) → colapsat.
|
False (first-run) → open; True (returning) → colapsat.
|
||||||
@@ -394,7 +394,7 @@
|
|||||||
border-bottom:none; border-radius:0;
|
border-bottom:none; border-radius:0;
|
||||||
margin-bottom:0; }
|
margin-bottom:0; }
|
||||||
/* === Sfarsit regiune accordion import === */
|
/* === Sfarsit regiune accordion import === */
|
||||||
/* === Inceput regiune nav links status-bar (US-005) ===
|
/* === Inceput regiune nav links status-bar ===
|
||||||
Linkuri Trimiteri + Mapari sub contoare; marcaj activ cu aria-current/status-nav-activ. */
|
Linkuri Trimiteri + Mapari sub contoare; marcaj activ cu aria-current/status-nav-activ. */
|
||||||
.status-nav-link { color:var(--accent); text-decoration:none; padding:2px 6px; border-radius:4px;
|
.status-nav-link { color:var(--accent); text-decoration:none; padding:2px 6px; border-radius:4px;
|
||||||
transition:background .1s; }
|
transition:background .1s; }
|
||||||
@@ -504,7 +504,7 @@
|
|||||||
/* === Preview import: coloane extra fata de tabelul Trimiteri.
|
/* === Preview import: coloane extra fata de tabelul Trimiteri.
|
||||||
SCOPAT prin .tabel-trimiteri (clasa partajata). Regiune separata —
|
SCOPAT prin .tabel-trimiteri (clasa partajata). Regiune separata —
|
||||||
nu atinge coloanele existente (col-chk/id/stare/data/rar/actualizat).
|
nu atinge coloanele existente (col-chk/id/stare/data/rar/actualizat).
|
||||||
US-007: 8 coloane (coloana de verificare manuala eliminata).
|
8 coloane (coloana de verificare manuala eliminata).
|
||||||
Suma latimi fixe: col-id(48) + col-stare(104) + col-data(104) +
|
Suma latimi fixe: col-id(48) + col-stare(104) + col-data(104) +
|
||||||
col-km(76) + col-note(176) + col-actiuni(92) = 600px.
|
col-km(76) + col-note(176) + col-actiuni(92) = 600px.
|
||||||
Restul (~680px la 1280px) revine lui col-vehicul + col-operatie (fluid). === */
|
Restul (~680px la 1280px) revine lui col-vehicul + col-operatie (fluid). === */
|
||||||
@@ -561,7 +561,7 @@
|
|||||||
font-size:12px; margin-bottom:2px; }
|
font-size:12px; margin-bottom:2px; }
|
||||||
.tabel-trimiteri td.col-chk, .tabel-trimiteri td.col-id { display:none; }
|
.tabel-trimiteri td.col-chk, .tabel-trimiteri td.col-id { display:none; }
|
||||||
|
|
||||||
/* === Card COMPACT (PRD 5.13, corectie dogfood) ===
|
/* === Card COMPACT ===
|
||||||
Inlocuieste stiva generica eticheta+valoare (prea inalta) cu un card
|
Inlocuieste stiva generica eticheta+valoare (prea inalta) cu un card
|
||||||
scanabil la prima vedere: vehicul = titlu, stare = pill dreapta-sus,
|
scanabil la prima vedere: vehicul = titlu, stare = pill dreapta-sus,
|
||||||
operatie+cod pe rand, meta (data/km/rar) muted mic, nota mica. Fara
|
operatie+cod pe rand, meta (data/km/rar) muted mic, nota mica. Fara
|
||||||
@@ -701,10 +701,10 @@
|
|||||||
.sticky-bar { padding:10px 12px; gap:10px; }
|
.sticky-bar { padding:10px 12px; gap:10px; }
|
||||||
.sticky-bar button { width:100%; min-height:44px; }
|
.sticky-bar button { width:100%; min-height:44px; }
|
||||||
}
|
}
|
||||||
/* === SENTINEL-COMPONENTE-SLIM: inceput componente slim US-002 (PRD 5.15).
|
/* === SENTINEL-COMPONENTE-SLIM: inceput componente slim.
|
||||||
Testele ancoreaza pe acest marker. Nu muta/sterge. === */
|
Testele ancoreaza pe acest marker. Nu muta/sterge. === */
|
||||||
/* .contor-card — card cifra contor: fundal --card2, bordura --line, radius 8px.
|
/* .contor-card — card cifra contor: fundal --card2, bordura --line, radius 8px.
|
||||||
US-002 PRD 5.16: padding marit (18px), cifra pe --fs-2xl, label pe --fs-sm, sub pe --fs-xs.
|
Padding marit (18px), cifra pe --fs-2xl, label pe --fs-sm, sub pe --fs-xs.
|
||||||
Variante de culoare a cifrei prin clasele .s-* existente (verde/accent/rosu). */
|
Variante de culoare a cifrei prin clasele .s-* existente (verde/accent/rosu). */
|
||||||
.contor-card { background:var(--card2); border:1px solid var(--line); border-radius:8px; padding:18px 18px; }
|
.contor-card { background:var(--card2); border:1px solid var(--line); border-radius:8px; padding:18px 18px; }
|
||||||
.contor-cifra { font-size:var(--fs-2xl); font-weight:700; line-height:1; }
|
.contor-cifra { font-size:var(--fs-2xl); font-weight:700; line-height:1; }
|
||||||
@@ -796,8 +796,8 @@
|
|||||||
@media (max-width:767px) {
|
@media (max-width:767px) {
|
||||||
.trimitere-slim { padding:12px 14px; }
|
.trimitere-slim { padding:12px 14px; }
|
||||||
}
|
}
|
||||||
/* === SENTINEL-COMPONENTE-SLIM: sfarsit componente slim US-002 === */
|
/* === SENTINEL-COMPONENTE-SLIM: sfarsit componente slim === */
|
||||||
/* === Fix mobil Mapari (bug live 2026-06-29) ===
|
/* === Fix mobil Mapari ===
|
||||||
Doua probleme raportate la 390px pe pagina Mapari:
|
Doua probleme raportate la 390px pe pagina Mapari:
|
||||||
(1) butoanele Salveaza/Sterge taiate: regula `.tabel-card td button {width:100%}`
|
(1) butoanele Salveaza/Sterge taiate: regula `.tabel-card td button {width:100%}`
|
||||||
(specificitate 0,1,2) batea `.act {width:44px}` (0,1,0) -> cele doua butoane act
|
(specificitate 0,1,2) batea `.act {width:44px}` (0,1,0) -> cele doua butoane act
|
||||||
@@ -823,10 +823,88 @@
|
|||||||
.tabel-card td .act .act-tx { display:inline; }
|
.tabel-card td .act .act-tx { display:inline; }
|
||||||
.tabel-card td .act .act-ic { display:inline-block; }
|
.tabel-card td .act .act-ic { display:inline-block; }
|
||||||
}
|
}
|
||||||
|
/* === Meniu hamburger grupat === */
|
||||||
|
/* Iconite SVG stroke unic, currentColor — mostenesc culoarea intrarii. */
|
||||||
|
.ic { width:18px; height:18px; stroke:currentColor; fill:none; stroke-width:1.7;
|
||||||
|
stroke-linecap:round; stroke-linejoin:round; flex-shrink:0; }
|
||||||
|
.cont-menu a, .cont-menu form button { display:flex; align-items:center; gap:10px; }
|
||||||
|
.cont-menu a .mi, .cont-menu form button .mi { display:inline-flex; opacity:.6; }
|
||||||
|
.cont-menu a:hover .mi, .cont-menu form button:hover .mi { opacity:.9; }
|
||||||
|
.cont-menu-group { font-size:10px; text-transform:uppercase; letter-spacing:.08em;
|
||||||
|
color:var(--muted); font-weight:700; padding:8px 12px 4px; }
|
||||||
|
/* Badge-ul de mapari se ancoreaza la dreapta chiar cand intrarea e flex. */
|
||||||
|
.cont-menu a .tab-badge { margin-left:auto; }
|
||||||
|
/* === Pagina Setari cu sub-taburi === */
|
||||||
|
.setari-head { margin-bottom:4px; }
|
||||||
|
.setari-title { font-size:var(--fs-lg); margin:0 0 2px; letter-spacing:-.01em;
|
||||||
|
display:flex; align-items:center; gap:9px; }
|
||||||
|
.setari-title .ic { width:20px; height:20px; opacity:.7; }
|
||||||
|
.setari-sub { color:var(--muted); font-size:var(--fs-sm); margin:0 0 14px; }
|
||||||
|
.setari-subtabs { display:flex; gap:2px; border-bottom:1px solid var(--line); margin-bottom:20px;
|
||||||
|
overflow-x:auto; -webkit-overflow-scrolling:touch; scrollbar-width:none; }
|
||||||
|
.setari-subtabs::-webkit-scrollbar { display:none; }
|
||||||
|
.setari-subtab { appearance:none; background:transparent; border:none; border-bottom:2px solid transparent;
|
||||||
|
color:var(--muted); font:inherit; font-size:var(--fs-sm); font-weight:500; cursor:pointer;
|
||||||
|
padding:10px 2px; margin-right:22px; white-space:nowrap; display:inline-flex;
|
||||||
|
align-items:center; gap:8px; margin-bottom:-1px; transition:color .12s, border-color .12s; }
|
||||||
|
.setari-subtab:hover { color:var(--ink); }
|
||||||
|
/* Stare activa prin clasa (nu selector pe atribut): aria-selected ramane pe buton
|
||||||
|
pentru accesibilitate, dar stilul nu depinde de el. */
|
||||||
|
.setari-subtab.is-active { color:var(--ink); border-bottom-color:var(--accent); font-weight:600; }
|
||||||
|
.setari-subtab:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
|
||||||
|
.st-dot { width:7px; height:7px; border-radius:99px; flex-shrink:0; }
|
||||||
|
.st-dot.ok { background:var(--ok); }
|
||||||
|
.st-dot.warn { background:var(--warn); }
|
||||||
|
.st-dot.info { background:var(--muted); }
|
||||||
|
.setari-pane[hidden] { display:none; }
|
||||||
|
.setari-zone-title { font-size:var(--fs-md); font-weight:600; margin:0 0 4px; display:flex;
|
||||||
|
align-items:center; gap:10px; flex-wrap:wrap; }
|
||||||
|
.setari-zone-title .ic { opacity:.7; }
|
||||||
|
.setari-zone-desc { color:var(--muted); font-size:var(--fs-sm); margin:0 0 16px; max-width:64ch; line-height:1.5; }
|
||||||
|
.setari-badge { font-size:11px; font-weight:700; padding:2px 9px; border-radius:99px;
|
||||||
|
text-transform:uppercase; letter-spacing:.03em; border:1px solid transparent; }
|
||||||
|
.setari-badge.sb-ok { color:var(--ok); background:color-mix(in srgb,var(--ok) 12%,transparent);
|
||||||
|
border-color:color-mix(in srgb,var(--ok) 40%,transparent); }
|
||||||
|
.setari-badge.sb-warn { color:var(--warn); background:color-mix(in srgb,var(--warn) 12%,transparent);
|
||||||
|
border-color:color-mix(in srgb,var(--warn) 40%,transparent); }
|
||||||
|
.setari-badge.sb-info { color:var(--muted); background:color-mix(in srgb,var(--muted) 12%,transparent);
|
||||||
|
border-color:color-mix(in srgb,var(--muted) 35%,transparent); }
|
||||||
|
.setari-subzone-sep { border:none; border-top:1px solid var(--line); margin:26px 0 22px; }
|
||||||
|
/* Cheie API afisata o singura data. */
|
||||||
|
.keybox { font-family:var(--font-mono); font-size:14px; background:var(--card2);
|
||||||
|
border:1px solid var(--line); border-radius:8px; padding:12px 14px; word-break:break-all;
|
||||||
|
max-width:480px; }
|
||||||
|
.btn-warn { background:transparent; color:var(--warn); border:1px solid var(--warn); border-radius:6px;
|
||||||
|
padding:8px 14px; font:inherit; font-weight:500; cursor:pointer; min-height:36px;
|
||||||
|
display:inline-flex; align-items:center; justify-content:center; gap:6px; }
|
||||||
|
.btn-warn:hover, .btn-warn:focus-visible { background:var(--warn); color:#fff; filter:none; }
|
||||||
|
/* Credentiale RAR: doua carduri de mediu paralele. */
|
||||||
|
.env-cards { display:grid; grid-template-columns:1fr 1fr; gap:14px; margin-bottom:18px; }
|
||||||
|
.env-card { border:1px solid var(--line); border-radius:10px; padding:16px; background:var(--card2); }
|
||||||
|
.env-card.active-test { border-color:color-mix(in srgb,var(--ok) 45%,var(--line)); }
|
||||||
|
.env-card h4 { margin:0 0 12px; font-size:var(--fs-sm); display:flex; align-items:center;
|
||||||
|
justify-content:space-between; gap:8px; }
|
||||||
|
.env-card .env-sw { display:flex; align-items:center; gap:8px; font-size:var(--fs-sm);
|
||||||
|
color:var(--muted); margin-bottom:12px; }
|
||||||
|
.env-card .env-lbl { display:block; font-size:var(--fs-xs); color:var(--muted); margin-bottom:5px; }
|
||||||
|
.env-card input[type=email], .env-card input[type=password] {
|
||||||
|
width:100%; font:inherit; background:var(--bg); color:var(--ink); border:1px solid var(--line);
|
||||||
|
border-radius:6px; padding:8px 10px; margin-bottom:10px; font-size:var(--fs-sm); }
|
||||||
|
.env-card .env-confirm { display:flex; align-items:flex-start; gap:8px; font-size:var(--fs-xs);
|
||||||
|
color:var(--muted); line-height:1.4;
|
||||||
|
background:color-mix(in srgb,var(--err) 7%,transparent);
|
||||||
|
border:1px solid color-mix(in srgb,var(--err) 22%,transparent);
|
||||||
|
border-radius:7px; padding:9px 11px; margin-top:2px; }
|
||||||
|
.env-card .env-confirm input { margin-top:2px; flex-shrink:0; }
|
||||||
|
.default-env { display:flex; align-items:center; gap:12px; flex-wrap:wrap; margin:0 0 18px;
|
||||||
|
padding:14px; border:1px solid var(--line); border-radius:9px; background:var(--card2); }
|
||||||
|
.default-env select { font:inherit; background:var(--bg); color:var(--ink); border:1px solid var(--line);
|
||||||
|
border-radius:6px; padding:7px 10px; max-width:340px; }
|
||||||
|
@media (max-width:640px) { .env-cards { grid-template-columns:1fr; } }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
{# US-010 (PRD 5.16): antet branduit ROA AUTOPASS.
|
{# Antet branduit ROA AUTOPASS.
|
||||||
Grila 3 coloane — stanga (logo) | centru (titlu+env+tier+account_name) | dreapta (RAR dot + tema + burger).
|
Grila 3 coloane — stanga (logo) | centru (titlu+env+tier+account_name) | dreapta (RAR dot + tema + burger).
|
||||||
Antet MINIMAL pe /login: nu afiseaza RAR dot, meniu burger sau account_name (nelogat). #}
|
Antet MINIMAL pe /login: nu afiseaza RAR dot, meniu burger sau account_name (nelogat). #}
|
||||||
<header>
|
<header>
|
||||||
@@ -850,7 +928,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{# Celula dreapta: dot RAR (numai cand logat) + selector tema + versiune + meniu burger #}
|
{# Celula dreapta: dot RAR (numai cand logat) + selector tema + versiune + meniu burger #}
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
{# US-003 (PRD 5.16): dot RAR in antet — OK = chip verde pulsant, BLOCAT = chip rosu.
|
{# Dot RAR in antet — OK = chip verde pulsant, BLOCAT = chip rosu.
|
||||||
Banda plina apare DOAR in _status.html cand BLOCAT (nu mai e mereu vizibila). #}
|
Banda plina apare DOAR in _status.html cand BLOCAT (nu mai e mereu vizibila). #}
|
||||||
{% if is_authenticated|default(false) %}
|
{% if is_authenticated|default(false) %}
|
||||||
{% if sanatate_ok|default(true) %}
|
{% if sanatate_ok|default(true) %}
|
||||||
@@ -868,11 +946,11 @@
|
|||||||
<span class="rar-tx">RAR blocat</span>
|
<span class="rar-tx">RAR blocat</span>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{# US-004 (PRD 5.19 / D1): comutatorul "Trimite automat la RAR" langa semnalul RAR.
|
{# Comutatorul "Trimite automat la RAR" langa semnalul RAR.
|
||||||
In afara swap-ului periodic al #status-bar (D3). #}
|
In afara swap-ului periodic al #status-bar. #}
|
||||||
{% include '_auto_send_toggle.html' %}
|
{% include '_auto_send_toggle.html' %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{# US-011 (PRD 5.16): selector tema = pill cu icon FIX (acelasi SVG ca landing) +
|
{# Selector tema = pill cu icon FIX (acelasi SVG ca landing) +
|
||||||
eticheta temei curente. Eticheta ascunsa pe <=560px via CSS. JS actualizeaza
|
eticheta temei curente. Eticheta ascunsa pe <=560px via CSS. JS actualizeaza
|
||||||
doar #tema-label (iconita nu se mai schimba per tema — consecventa cu landing). #}
|
doar #tema-label (iconita nu se mai schimba per tema — consecventa cu landing). #}
|
||||||
<button id="tema-toggle" class="tema-btn"
|
<button id="tema-toggle" class="tema-btn"
|
||||||
@@ -885,14 +963,14 @@
|
|||||||
<span class="muted" style="font-size:var(--fs-xs);">v{{ version }}</span>
|
<span class="muted" style="font-size:var(--fs-xs);">v{{ version }}</span>
|
||||||
{% if is_authenticated|default(false) %}
|
{% if is_authenticated|default(false) %}
|
||||||
{# Meniu cont: Cont/Integrare/Nomenclator + (admin) + logout.
|
{# Meniu cont: Cont/Integrare/Nomenclator + (admin) + logout.
|
||||||
US-010: structura cu <hr> separatori + RAR status (prima intrare) + Plan tier.
|
Structura cu <hr> separatori + RAR status (prima intrare) + Plan tier.
|
||||||
Pe paginile neautentificate (login/signup) nu se randeaza deloc. #}
|
Pe paginile neautentificate (login/signup) nu se randeaza deloc. #}
|
||||||
<div class="cont-menu-wrap">
|
<div class="cont-menu-wrap">
|
||||||
<button id="cont-menu-toggle" class="icon-btn"
|
<button id="cont-menu-toggle" class="icon-btn"
|
||||||
aria-haspopup="true" aria-expanded="false" aria-controls="cont-menu"
|
aria-haspopup="true" aria-expanded="false" aria-controls="cont-menu"
|
||||||
aria-label="Meniu cont" title="Meniu cont">☰</button>
|
aria-label="Meniu cont" title="Meniu cont">☰</button>
|
||||||
<div id="cont-menu" class="cont-menu" role="menu" aria-labelledby="cont-menu-toggle" hidden>
|
<div id="cont-menu" class="cont-menu" role="menu" aria-labelledby="cont-menu-toggle" hidden>
|
||||||
{# Prima intrare: starea RAR (US-003) #}
|
{# Prima intrare: starea RAR #}
|
||||||
{% if sanatate_ok|default(true) %}
|
{% if sanatate_ok|default(true) %}
|
||||||
<div class="menu-rar-line ok" role="menuitem" aria-disabled="true">
|
<div class="menu-rar-line ok" role="menuitem" aria-disabled="true">
|
||||||
<span style="width:8px;height:8px;border-radius:99px;background:currentColor;display:inline-block;"></span>
|
<span style="width:8px;height:8px;border-radius:99px;background:currentColor;display:inline-block;"></span>
|
||||||
@@ -904,31 +982,31 @@
|
|||||||
RAR indisponibil
|
RAR indisponibil
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{# Plan cont curent (US-006 PRD 5.17): linie detaliata cu trial/consum/warn.
|
{# Plan cont curent: linie detaliata cu trial/consum/warn.
|
||||||
Warn = culoare + text (accesibilitate, decizie #14). #}
|
Warn = culoare + text (accesibilitate). #}
|
||||||
<div class="menu-rar-line" role="menuitem" aria-disabled="true"
|
<div class="menu-rar-line" role="menuitem" aria-disabled="true"
|
||||||
style="color:{% if plan_limita_atinsa|default(false) %}var(--err){% elif plan_warn|default(false) %}var(--warn){% else %}var(--muted){% endif %};
|
style="color:{% if plan_limita_atinsa|default(false) %}var(--err){% elif plan_warn|default(false) %}var(--warn){% else %}var(--muted){% endif %};
|
||||||
{% if plan_warn|default(false) %}font-weight:600;{% endif %}">
|
{% if plan_warn|default(false) %}font-weight:600;{% endif %}">
|
||||||
{{ plan_linie|default('Plan: ' + (tier_label|default('Gratuit'))) }}
|
{{ plan_linie|default('Plan: ' + (tier_label|default('Gratuit'))) }}
|
||||||
</div>
|
</div>
|
||||||
<hr>
|
<hr>
|
||||||
{# Navigare principala: Trimiteri + Mapari #}
|
{# Grup LUCRU: fluxul zilnic — trimiteri, mapari, nomenclator. #}
|
||||||
<a role="menuitem" href="/">Trimiteri</a>
|
<div class="cont-menu-group">Lucru</div>
|
||||||
|
<a role="menuitem" href="/"><span class="mi"><svg class="ic" viewBox="0 0 24 24" aria-hidden="true"><path d="M22 2 11 13"/><path d="M22 2 15 22l-4-9-9-4z"/></svg></span>Trimiteri</a>
|
||||||
{% set _mapari_badge = (badges.mapari if (badges is defined and badges and badges.mapari) else 0) %}
|
{% set _mapari_badge = (badges.mapari if (badges is defined and badges and badges.mapari) else 0) %}
|
||||||
<a role="menuitem" href="/?tab=mapari">Mapari{% if _mapari_badge %}<span class="tab-badge" aria-hidden="true" style="display:inline-flex; align-items:center; justify-content:center; min-width:18px; height:18px; margin-left:6px; padding:0 5px; border-radius:99px; background:var(--err); color:#fff; font-size:11px; font-weight:700;">{{ _mapari_badge }}</span>{% endif %}</a>
|
<a role="menuitem" href="/?tab=mapari"><span class="mi"><svg class="ic" viewBox="0 0 24 24" aria-hidden="true"><path d="M10 14 14 10"/><path d="M11.5 6.5 13 5a4 4 0 0 1 6 6l-1.5 1.5"/><path d="M12.5 17.5 11 19a4 4 0 0 1-6-6l1.5-1.5"/></svg></span>Mapari{% if _mapari_badge %}<span class="tab-badge" aria-hidden="true" style="display:inline-flex; align-items:center; justify-content:center; min-width:18px; height:18px; padding:0 5px; border-radius:99px; background:var(--err); color:#fff; font-size:11px; font-weight:700;">{{ _mapari_badge }}</span>{% endif %}</a>
|
||||||
|
<a role="menuitem" href="/?tab=nomenclator"><span class="mi"><svg class="ic" viewBox="0 0 24 24" aria-hidden="true"><path d="M5 4a1 1 0 0 1 1-1h13v18H6a1 1 0 0 0-1 1z"/><path d="M5 18h14"/></svg></span>Nomenclator</a>
|
||||||
<hr>
|
<hr>
|
||||||
{# Nomenclator: coduri RAR — public, dar in meniu arata mai logic la logat #}
|
{# Grup CONT: configurare + audit + admin. "Cont" redenumit "Setari" (URL ramane ?tab=cont). #}
|
||||||
<a role="menuitem" href="/?tab=nomenclator">Nomenclator</a>
|
<div class="cont-menu-group">Cont</div>
|
||||||
<hr>
|
<a role="menuitem" href="/?tab=cont"><span class="mi"><svg class="ic" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="3"/><path d="M12 2v3M12 19v3M2 12h3M19 12h3M4.6 4.6l2.1 2.1M17.3 17.3l2.1 2.1M4.6 19.4l2.1-2.1M17.3 6.7l2.1-2.1"/></svg></span>Setari</a>
|
||||||
{# Setari cont #}
|
<a role="menuitem" href="/?tab=integrare"><span class="mi"><svg class="ic" viewBox="0 0 24 24" aria-hidden="true"><path d="M9 3v6M15 3v6"/><path d="M7 9h10v3a5 5 0 0 1-10 0z"/><path d="M12 17v4"/></svg></span>Integrare</a>
|
||||||
<a role="menuitem" href="/?tab=cont">Cont</a>
|
<a role="menuitem" href="/?tab=jurnal"><span class="mi"><svg class="ic" viewBox="0 0 24 24" aria-hidden="true"><path d="M7 3h8l4 4v14H7z"/><path d="M15 3v4h4"/><path d="M10 13h6M10 17h6"/></svg></span>Jurnal</a>
|
||||||
<a role="menuitem" href="/?tab=integrare">Integrare</a>
|
{% if is_admin|default(false) %}<a role="menuitem" href="/admin"><span class="mi"><svg class="ic" viewBox="0 0 24 24" aria-hidden="true"><circle cx="9" cy="8" r="3"/><path d="M3.5 20a5.5 5.5 0 0 1 11 0"/><path d="M16 5.5a3 3 0 0 1 0 5"/><path d="M17.5 14.2A5.5 5.5 0 0 1 21 20"/></svg></span>Conturi clienti</a>{% endif %}
|
||||||
<a role="menuitem" href="/?tab=jurnal">Jurnal</a>
|
|
||||||
{% if is_admin|default(false) %}<a role="menuitem" href="/admin">Conturi clienti</a>{% endif %}
|
|
||||||
<hr>
|
<hr>
|
||||||
<form method="post" action="/logout">
|
<form method="post" action="/logout">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token|default('') }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token|default('') }}">
|
||||||
<button role="menuitem" type="submit">Iesi din cont</button>
|
<button role="menuitem" type="submit"><span class="mi"><svg class="ic" viewBox="0 0 24 24" aria-hidden="true"><path d="M14 8V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h7a2 2 0 0 0 2-2v-3"/><path d="M9 12h12"/><path d="m18 9 3 3-3 3"/></svg></span>Iesi din cont</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -954,12 +1032,12 @@
|
|||||||
<div id="detaliu-modal-body"></div>
|
<div id="detaliu-modal-body"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{# Slot pentru modalul de confirmare tipata (PRD 5.19 D4): auto-release OFF->ON injecteaza
|
{# Slot pentru modalul de confirmare tipata: auto-release OFF->ON injecteaza
|
||||||
aici modalul via OOB (#modal-confirma-slot). Bulk "Trimite toate" isi randeaza propriul
|
aici modalul via OOB (#modal-confirma-slot). Bulk "Trimite toate" isi randeaza propriul
|
||||||
modal inline in _coada.html. #}
|
modal inline in _coada.html. #}
|
||||||
<div id="modal-confirma-slot"></div>
|
<div id="modal-confirma-slot"></div>
|
||||||
<script>
|
<script>
|
||||||
// Comutator tema ciclic (DRY E2 — PRD 5.15): config traieste intr-o singura structura
|
// Comutator tema ciclic: config traieste intr-o singura structura
|
||||||
// sursa-de-adevar THEMES din care se DERIVA CYCLE/VALID/LABELS/NEXT.
|
// sursa-de-adevar THEMES din care se DERIVA CYCLE/VALID/LABELS/NEXT.
|
||||||
// Adaugarea unei teme noi = O singura intrare in THEMES.
|
// Adaugarea unei teme noi = O singura intrare in THEMES.
|
||||||
// Ciclu: Light->Dark->Petrol->Grafit->Cobalt->Cupru->Hartie->Auto->(inapoi la Light).
|
// Ciclu: Light->Dark->Petrol->Grafit->Cobalt->Cupru->Hartie->Auto->(inapoi la Light).
|
||||||
@@ -997,7 +1075,7 @@
|
|||||||
}
|
}
|
||||||
function _syncButton(stored) {
|
function _syncButton(stored) {
|
||||||
var s = VALID[stored] ? stored : 'auto';
|
var s = VALID[stored] ? stored : 'auto';
|
||||||
// US-011: iconita e fixa (nu se schimba per tema); actualizam doar eticheta.
|
// Iconita e fixa (nu se schimba per tema); actualizam doar eticheta.
|
||||||
var label = document.getElementById('tema-label');
|
var label = document.getElementById('tema-label');
|
||||||
if (label) label.textContent = LABELS[s];
|
if (label) label.textContent = LABELS[s];
|
||||||
btn.setAttribute('aria-label', 'Tema: ' + LABELS[s] + ', apasa pentru ' + NEXT[s]);
|
btn.setAttribute('aria-label', 'Tema: ' + LABELS[s] + ', apasa pentru ' + NEXT[s]);
|
||||||
@@ -1109,6 +1187,28 @@
|
|||||||
if (saveBtn) saveBtn.classList.add('dirty');
|
if (saveBtn) saveBtn.classList.add('dirty');
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
<script>
|
||||||
|
// Sub-taburi pagina Setari: comutare pur client-side, fara server.
|
||||||
|
// Delegare pe document -> supravietuieste swap-urilor HTMX ale #card-cont (dupa
|
||||||
|
// fiecare salvare serverul re-randeaza cu sub-tabul corect via `aria-selected`/`hidden`,
|
||||||
|
// iar acest handler preia doar navigarea manuala intre taburi).
|
||||||
|
document.addEventListener('click', function(e) {
|
||||||
|
var tab = e.target.closest('.setari-subtab');
|
||||||
|
if (!tab) return;
|
||||||
|
var card = tab.closest('#card-cont');
|
||||||
|
if (!card) return;
|
||||||
|
var name = tab.getAttribute('data-subtab');
|
||||||
|
card.querySelectorAll('.setari-subtab').forEach(function(t) {
|
||||||
|
var on = t === tab;
|
||||||
|
t.setAttribute('aria-selected', on ? 'true' : 'false');
|
||||||
|
t.classList.toggle('is-active', on);
|
||||||
|
});
|
||||||
|
card.querySelectorAll('.setari-pane').forEach(function(p) {
|
||||||
|
if (p.getAttribute('data-subpane') === name) { p.removeAttribute('hidden'); }
|
||||||
|
else { p.setAttribute('hidden', ''); }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
<script>
|
<script>
|
||||||
// Toast global: feedback tranzitoriu vizibil + accesibil (aria-live).
|
// Toast global: feedback tranzitoriu vizibil + accesibil (aria-live).
|
||||||
// window.arataToast(text, stareCss) — stareCss (ex. "s-error"/"s-needs_review")
|
// window.arataToast(text, stareCss) — stareCss (ex. "s-error"/"s-needs_review")
|
||||||
@@ -1274,7 +1374,7 @@
|
|||||||
// Deschidere la click pe rand (htmx:beforeRequest): arata modalul cu placeholder
|
// Deschidere la click pe rand (htmx:beforeRequest): arata modalul cu placeholder
|
||||||
// inainte ca raspunsul fragmentului sa fie swap-uit in corp.
|
// inainte ca raspunsul fragmentului sa fie swap-uit in corp.
|
||||||
// Trateaza atat .trimitere-row (Trimiteri) cat si .btn-editeaza (preview import)
|
// Trateaza atat .trimitere-row (Trimiteri) cat si .btn-editeaza (preview import)
|
||||||
// → open() instaleaza inert pe <main>, focus-trap si readuce focusul la inchidere (US-006).
|
// → open() instaleaza inert pe <main>, focus-trap si readuce focusul la inchidere.
|
||||||
document.body.addEventListener('htmx:beforeRequest', function(evt) {
|
document.body.addEventListener('htmx:beforeRequest', function(evt) {
|
||||||
var elt = evt.detail && evt.detail.elt;
|
var elt = evt.detail && evt.detail.elt;
|
||||||
if (!elt || !elt.classList) return;
|
if (!elt || !elt.classList) return;
|
||||||
@@ -1387,7 +1487,7 @@
|
|||||||
// Daca difera (schimbari externe — ex. worker a procesat trimiteri), reincarca automat
|
// Daca difera (schimbari externe — ex. worker a procesat trimiteri), reincarca automat
|
||||||
// pastrand filtrul curent. Fara nudge "Date noi" — auto-refresh e mai consistent.
|
// pastrand filtrul curent. Fara nudge "Date noi" — auto-refresh e mai consistent.
|
||||||
// Decizie: nudge eliminat; distinctia propriu vs extern e imposibila pe client
|
// Decizie: nudge eliminat; distinctia propriu vs extern e imposibila pe client
|
||||||
// fara sesiune dedicata — auto-refresh acoper ambele cazuri (US-008).
|
// fara sesiune dedicata — auto-refresh acoper ambele cazuri.
|
||||||
var INTERVAL = 20000;
|
var INTERVAL = 20000;
|
||||||
function versiuneCurenta() {
|
function versiuneCurenta() {
|
||||||
var e = document.getElementById('trimiteri-versiune');
|
var e = document.getElementById('trimiteri-versiune');
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<meta name="description" content="Încarci fișierul tău cu operațiile service-ului, completezi o dată codurile RAR și le salvezi. ROMFAST trimite prestațiile la RAR AUTOPASS în locul tău, fără tastat manual. Conform Legii 142/2023.">
|
<meta name="description" content="Încarci fișierul tău cu operațiile service-ului, completezi o dată codurile RAR și le salvezi. ROMFAST trimite prestațiile la RAR AUTOPASS în locul tău, fără tastat manual. Conform Legii 142/2023.">
|
||||||
<style>
|
<style>
|
||||||
|
|
||||||
/* US-001/US-008 (PRD 5.16): IBM Plex eliminat complet — stive font sistem standard web.
|
/* IBM Plex eliminat complet — stive font sistem standard web.
|
||||||
Tokenurile --font-ui / --font-mono definite in :root (sursa unica de adevar). */
|
Tokenurile --font-ui / --font-mono definite in :root (sursa unica de adevar). */
|
||||||
:root{--font-ui:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;--font-mono:ui-monospace,"SF Mono","Cascadia Code","Segoe UI Mono","Roboto Mono",Menlo,Consolas,monospace;}
|
:root{--font-ui:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;--font-mono:ui-monospace,"SF Mono","Cascadia Code","Segoe UI Mono","Roboto Mono",Menlo,Consolas,monospace;}
|
||||||
*{box-sizing:border-box;}
|
*{box-sizing:border-box;}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
{% block title %}Autentificare — ROA AUTOPASS{% endblock %}
|
{% block title %}Autentificare — ROA AUTOPASS{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{# /login — card de autentificare branduit, o singura coloana (simplificat din layout-ul
|
{# /login — card de autentificare branduit, o singura coloana (simplificat din layout-ul
|
||||||
2-coloane US-010 PRD 5.16). Formular: CSRF, POST /login, link signup. #}
|
2-coloane). Formular: CSRF, POST /login, link signup. #}
|
||||||
<div class="login-2col" style="max-width:860px; margin:32px auto;">
|
<div class="login-2col" style="max-width:860px; margin:32px auto;">
|
||||||
{# Antet minimal deja randat in base.html (fara RAR dot, fara burger, fara account_name) #}
|
{# Antet minimal deja randat in base.html (fara RAR dot, fara burger, fara account_name) #}
|
||||||
<div class="login-shell">
|
<div class="login-shell">
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ Worker-ul face login per CONT cu acele creds, cache-uieste JWT (30h) in memorie
|
|||||||
STERGE creds-urile efemere dupa primul login reusit. Token-ul in memorie acopera
|
STERGE creds-urile efemere dupa primul login reusit. Token-ul in memorie acopera
|
||||||
restul trimiterilor; la restart token-ul se pierde si contul re-logheaza la urmatorul
|
restul trimiterilor; la restart token-ul se pierde si contul re-logheaza la urmatorul
|
||||||
submission care aduce creds proaspete (degradare acceptata). Fallback durabil: slotul
|
submission care aduce creds proaspete (degradare acceptata). Fallback durabil: slotul
|
||||||
per-env al contului (accounts.rar_creds_{env}_enc, US-013; coloana legacy dropata).
|
per-env al contului (accounts.rar_creds_{env}_enc; coloana legacy dropata).
|
||||||
Dev: `worker_use_test_creds` foloseste creds <test> cand submission-ul nu are enc.
|
Dev: `worker_use_test_creds` foloseste creds <test> cand submission-ul nu are enc.
|
||||||
|
|
||||||
Pornire: python -m app.worker
|
Pornire: python -m app.worker
|
||||||
@@ -145,7 +145,7 @@ def purge_expired(conn) -> dict[str, int]:
|
|||||||
def expire_held(conn, settings: Settings) -> int:
|
def expire_held(conn, settings: Settings) -> int:
|
||||||
"""Expira randurile tinute manual (`queued AND held=1`) mai vechi de `held_retention_days`.
|
"""Expira randurile tinute manual (`queued AND held=1`) mai vechi de `held_retention_days`.
|
||||||
|
|
||||||
PRD 5.19 US-008 (inchide gaura GDPR/L.142): un `queued held=1` nu e nici `sent` nici
|
Inchide gaura GDPR/L.142: un `queued held=1` nu e nici `sent` nici
|
||||||
blocat -> altfel NU primeste `purge_after` -> PII criptat ar sta la nesfarsit.
|
blocat -> altfel NU primeste `purge_after` -> PII criptat ar sta la nesfarsit.
|
||||||
Le trece la `error`/`TINUT_EXPIRAT` (terminal) si seteaza `purge_after` DIRECT la
|
Le trece la `error`/`TINUT_EXPIRAT` (terminal) si seteaza `purge_after` DIRECT la
|
||||||
momentul expirarii (NU lasa retentia de 30z a randurilor blocate sa se adauge; altfel
|
momentul expirarii (NU lasa retentia de 30z a randurilor blocate sa se adauge; altfel
|
||||||
@@ -154,7 +154,7 @@ def expire_held(conn, settings: Settings) -> int:
|
|||||||
(`purge_after < datetime('now')`), iar `datetime('now')` are rezolutie de o secunda —
|
(`purge_after < datetime('now')`), iar `datetime('now')` are rezolutie de o secunda —
|
||||||
cu `purge_after=now` cele doua ar fi egale in acelasi ciclu si randul NU s-ar purja
|
cu `purge_after=now` cele doua ar fi egale in acelasi ciclu si randul NU s-ar purja
|
||||||
decat la ciclul urmator (~1h de PII in plus). Cu `-1 second` randul e purjabil IMEDIAT
|
decat la ciclul urmator (~1h de PII in plus). Cu `-1 second` randul e purjabil IMEDIAT
|
||||||
in acelasi ciclu de purjare (US-008 inchide gaura strict).
|
in acelasi ciclu de purjare.
|
||||||
|
|
||||||
Intoarce numarul de randuri expirate.
|
Intoarce numarul de randuri expirate.
|
||||||
"""
|
"""
|
||||||
@@ -199,7 +199,7 @@ def claim_one(conn) -> dict | None:
|
|||||||
"SELECT s.id, s.account_id, s.payload_json, s.rar_creds_enc, s.rar_env "
|
"SELECT s.id, s.account_id, s.payload_json, s.rar_creds_enc, s.rar_env "
|
||||||
"FROM submissions s LEFT JOIN accounts a ON a.id = s.account_id "
|
"FROM submissions s LEFT JOIN accounts a ON a.id = s.account_id "
|
||||||
"WHERE s.status='queued' "
|
"WHERE s.status='queued' "
|
||||||
# PRD 5.19 US-003: randurile tinute manual (held=1) NU se revendica —
|
# Randurile tinute manual (held=1) NU se revendica —
|
||||||
# asteapta eliberarea umana (manual/auto). Se aplica DOAR la claim din
|
# asteapta eliberarea umana (manual/auto). Se aplica DOAR la claim din
|
||||||
# 'queued'; randurile deja 'sending' (orfani/reconciliere) sunt neafectate.
|
# 'queued'; randurile deja 'sending' (orfani/reconciliere) sunt neafectate.
|
||||||
"AND s.held = 0 "
|
"AND s.held = 0 "
|
||||||
@@ -464,7 +464,7 @@ def _creds_for(claimed: dict, settings: Settings) -> dict | None:
|
|||||||
|
|
||||||
|
|
||||||
def _creds_from_account(conn, account_id: int, rar_env: str = "test") -> dict | None:
|
def _creds_from_account(conn, account_id: int, rar_env: str = "test") -> dict | None:
|
||||||
"""Creds RAR durabile per-cont din slotul per-env (US-013 — coloana legacy dropata).
|
"""Creds RAR durabile per-cont din slotul per-env (coloana legacy dropata).
|
||||||
|
|
||||||
Canal web: creds in accounts.rar_creds_{rar_env}_enc (per-env, singurul slot valid).
|
Canal web: creds in accounts.rar_creds_{rar_env}_enc (per-env, singurul slot valid).
|
||||||
"""
|
"""
|
||||||
@@ -482,7 +482,7 @@ def _keepalive_target(conn, settings: Settings) -> tuple[int | None, dict | None
|
|||||||
"""Un cont cu creds durabile pentru login-ul de proba (sau creds <test> in dev).
|
"""Un cont cu creds durabile pentru login-ul de proba (sau creds <test> in dev).
|
||||||
|
|
||||||
Ancora M2: cauta in slotul per-env al mediului `settings.rar_env` (ancora globala).
|
Ancora M2: cauta in slotul per-env al mediului `settings.rar_env` (ancora globala).
|
||||||
US-013: coloana legacy accounts.rar_creds_enc a fost dropata — se foloseste EXCLUSIV
|
Coloana legacy accounts.rar_creds_enc a fost dropata — se foloseste EXCLUSIV
|
||||||
slotul per-env. Sare conturile ale caror creds NU se decripteaza sub cheia curenta
|
slotul per-env. Sare conturile ale caror creds NU se decripteaza sub cheia curenta
|
||||||
(in dev `start.sh both` genereaza o cheie efemera noua la fiecare pornire).
|
(in dev `start.sh both` genereaza o cheie efemera noua la fiecare pornire).
|
||||||
"""
|
"""
|
||||||
@@ -565,7 +565,7 @@ def run() -> int:
|
|||||||
# Purjare periodica (odata pe ora) — NU mai frecvent.
|
# Purjare periodica (odata pe ora) — NU mai frecvent.
|
||||||
now_ts = time.time()
|
now_ts = time.time()
|
||||||
if now_ts - _last_purge_time >= _PURGE_INTERVAL_S:
|
if now_ts - _last_purge_time >= _PURGE_INTERVAL_S:
|
||||||
# Expira intai randurile tinute imbatranite (US-008), apoi purjeaza:
|
# Expira intai randurile tinute imbatranite, apoi purjeaza:
|
||||||
# expire_held seteaza purge_after=now-1s -> randul e purjabil IMEDIAT in
|
# expire_held seteaza purge_after=now-1s -> randul e purjabil IMEDIAT in
|
||||||
# acelasi ciclu (purge_expired foloseste comparatie stricta purge_after<now).
|
# acelasi ciclu (purge_expired foloseste comparatie stricta purge_after<now).
|
||||||
expired = expire_held(conn, settings)
|
expired = expire_held(conn, settings)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Liveness probe pentru worker (T6) — folosit de healthcheck-ul Docker.
|
"""Liveness probe pentru worker — folosit de healthcheck-ul Docker.
|
||||||
|
|
||||||
Worker-ul nu e server HTTP, deci `restart: always` prinde doar procesul MORT,
|
Worker-ul nu e server HTTP, deci `restart: always` prinde doar procesul MORT,
|
||||||
nu si worker-ul AGATAT (proces viu care nu mai bate heartbeat). Acest probe
|
nu si worker-ul AGATAT (proces viu care nu mai bate heartbeat). Acest probe
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ services:
|
|||||||
- autopass-data:/data
|
- autopass-data:/data
|
||||||
environment:
|
environment:
|
||||||
AUTOPASS_DB_PATH: /data/autopass.db
|
AUTOPASS_DB_PATH: /data/autopass.db
|
||||||
# Ancora globala, NU tinta trimiterilor: dupa PRD 5.20 mediul RAR e per
|
# Ancora globala, NU tinta trimiterilor: mediul RAR e per
|
||||||
# submission/cont (worker-ul trimite dupa submissions.rar_env). Variabila
|
# submission/cont (worker-ul trimite dupa submissions.rar_env). Variabila
|
||||||
# asta serveste doar ca fallback API pentru conturi fara medii disponibile,
|
# asta serveste doar ca fallback API pentru conturi fara medii disponibile,
|
||||||
# backfill la migrare (db.py) si afisaj dashboard (routes.py). Obligatorie
|
# backfill la migrare (db.py) si afisaj dashboard (routes.py). Obligatorie
|
||||||
|
|||||||
@@ -22,9 +22,9 @@ os.environ.setdefault("AUTOPASS_WORKER_USE_TEST_CREDS", "false")
|
|||||||
# lazy-load-eze modelul de ~230MB la fiecare test care atinge editorul de mapari
|
# lazy-load-eze modelul de ~230MB la fiecare test care atinge editorul de mapari
|
||||||
# (suita rapida, fara download in CI). Testele de embeddings il pornesc punctual.
|
# (suita rapida, fara download in CI). Testele de embeddings il pornesc punctual.
|
||||||
os.environ.setdefault("AUTOPASS_EMBEDDINGS_ENABLED", "false")
|
os.environ.setdefault("AUTOPASS_EMBEDDINGS_ENABLED", "false")
|
||||||
# Seed-ul de operatii etichetate (SILVER, PRD 5.18) e ON in app, dar OFF in teste:
|
# Seed-ul de operatii etichetate e ON in app, dar OFF in teste: multe teste presupun
|
||||||
# multe teste presupun mapping_suggestions GOL la init_db. Testele US-004/005/006 il
|
# mapping_suggestions GOL la init_db. Testele care au nevoie de el il pornesc punctual
|
||||||
# pornesc punctual (object.__setattr__ pe settings sau apel direct la seeder).
|
# (object.__setattr__ pe settings sau apel direct la seeder).
|
||||||
os.environ.setdefault("AUTOPASS_SEED_OPERATII_ENABLED", "false")
|
os.environ.setdefault("AUTOPASS_SEED_OPERATII_ENABLED", "false")
|
||||||
|
|
||||||
|
|
||||||
@@ -35,15 +35,15 @@ _REAL_INIT_DB = None
|
|||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def _auto_send_on_default_account(request, monkeypatch):
|
def _auto_send_on_default_account(request, monkeypatch):
|
||||||
"""PRD 5.19 US-009: contul default id=1 (seed schema.sql, INSERT OR IGNORE) porneste pe
|
"""Contul default id=1 (seed schema.sql, INSERT OR IGNORE) porneste pe
|
||||||
`auto_send_enabled=0` (Auto OFF) — corect in productie (nimic nu pleaca fara confirmare).
|
`auto_send_enabled=0` (Auto OFF) — corect in productie (nimic nu pleaca fara confirmare).
|
||||||
|
|
||||||
Dar suita exercita lantul `POST -> claim_one -> sent` pe contul id=1
|
Dar suita exercita lantul `POST -> claim_one -> sent` pe contul id=1
|
||||||
(test_import_e2e, test_creds_delivery, test_live_rar): cu Auto OFF, ingestia ar marca
|
(test_import_e2e, test_creds_delivery, test_live_rar): cu Auto OFF, ingestia ar marca
|
||||||
`held=1` (US-002) si worker-ul (`claim_one ... AND held=0`, US-003) nu ar mai lua randul
|
`held=1` si worker-ul (`claim_one ... AND held=0`) nu ar mai lua randul
|
||||||
-> testele ar stagna tacit. E un fix de STARE DB, nu env var (coloana e per-rand in
|
-> testele ar stagna tacit. E un fix de STARE DB, nu env var (coloana e per-rand in
|
||||||
`accounts`), deci il aplicam wraparind `init_db` (contul id=1 NU e creat de `create_account`
|
`accounts`), deci il aplicam wraparind `init_db` (contul id=1 NU e creat de `create_account`
|
||||||
-> un patch pe factory NU l-ar acoperi — Eng HIGH).
|
-> un patch pe factory NU l-ar acoperi).
|
||||||
|
|
||||||
Wrapam `init_db` astfel incat orice DB de test creat sa aiba id=1 pe Auto ON (held=0 la
|
Wrapam `init_db` astfel incat orice DB de test creat sa aiba id=1 pe Auto ON (held=0 la
|
||||||
ingestie), pastrand suita verde ca inainte de 5.19. Conturile create explicit de teste
|
ingestie), pastrand suita verde ca inainte de 5.19. Conturile create explicit de teste
|
||||||
@@ -114,7 +114,7 @@ def _reset_embeddings_singleton():
|
|||||||
|
|
||||||
|
|
||||||
def make_test_cui(seed: str = "") -> str:
|
def make_test_cui(seed: str = "") -> str:
|
||||||
"""Factory centralizat (D#14, PRD 5.12 US-001): genereaza un CUI de test unic din seed.
|
"""Factory centralizat: genereaza un CUI de test unic din seed.
|
||||||
|
|
||||||
Folosit de fixture-urile de test care creeaza conturi via /signup sau create_account
|
Folosit de fixture-urile de test care creeaza conturi via /signup sau create_account
|
||||||
si au nevoie de un CUI unic per test (altfel unicitatea CUI-ului bloca al doilea signup
|
si au nevoie de un CUI unic per test (altfel unicitatea CUI-ului bloca al doilea signup
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
"""Teste US-003 (PRD 3.6): Acasa unificata — Trimiteri ca sectiune permanenta.
|
"""Teste Acasa unificata — Trimiteri ca sectiune permanenta.
|
||||||
|
|
||||||
Tab-ul "Trimiteri" (coada) eliminat din tab-bar; Trimiterile devin o sectiune sub
|
Tab-ul "Trimiteri" (coada) eliminat din tab-bar; Trimiterile devin o sectiune sub
|
||||||
upload pe Acasa, cu heading "Trimiterile tale". ?tab=coada si /_fragments/coada
|
upload pe Acasa, cu heading "Trimiterile tale". ?tab=coada si /_fragments/coada
|
||||||
servesc continutul Acasa (nu 404, nu fragment orfan). Poll aliniat la 15s (M5).
|
servesc continutul Acasa (nu 404, nu fragment orfan).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -50,14 +50,14 @@ def _seed_submission(status: str = "sent", n: int = 1) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_tab_bar_fara_trimiteri(client):
|
def test_tab_bar_fara_trimiteri(client):
|
||||||
"""US-009: tab-bar eliminat; 'Coada' nu exista; Mapari/Cont/Nomenclator raman in meniu."""
|
"""Tab-bar eliminat; 'Coada' nu exista; Mapari/Cont/Nomenclator raman in meniu."""
|
||||||
r = client.get("/")
|
r = client.get("/")
|
||||||
assert r.status_code == 200
|
assert r.status_code == 200
|
||||||
html = r.text
|
html = r.text
|
||||||
# "Coada" nu trebuie sa existe nici ca tab, nici ca link in meniu
|
# "Coada" nu trebuie sa existe nici ca tab, nici ca link in meniu
|
||||||
assert 'id="tab-coada"' not in html
|
assert 'id="tab-coada"' not in html
|
||||||
assert 'href="/?tab=coada"' not in html
|
assert 'href="/?tab=coada"' not in html
|
||||||
# US-009: tab-bar eliminat; Mapari/Cont/Nomenclator sunt in meniul hamburger
|
# Mapari/Cont/Nomenclator sunt in meniul hamburger
|
||||||
for label in ("Mapari", "Cont", "Nomenclator"):
|
for label in ("Mapari", "Cont", "Nomenclator"):
|
||||||
assert f">{label}" in html or f"{label}<" in html, f"lipseste intrarea {label} in meniu"
|
assert f">{label}" in html or f"{label}<" in html, f"lipseste intrarea {label} in meniu"
|
||||||
|
|
||||||
@@ -70,8 +70,7 @@ def test_acasa_contine_sectiunea_trimiteri(client):
|
|||||||
html = r.text
|
html = r.text
|
||||||
assert 'id="filtre-trimiteri"' in html
|
assert 'id="filtre-trimiteri"' in html
|
||||||
assert "/_fragments/submissions" in html
|
assert "/_fragments/submissions" in html
|
||||||
# PRD 5.9 US-003: detaliul s-a mutat in modalul global (#modal-detaliu); vechiul
|
# Detaliul e in modalul global (#modal-detaliu); panoul inert vechi eliminat.
|
||||||
# panou inert #trimitere-detaliu a fost eliminat.
|
|
||||||
assert 'id="modal-detaliu"' in html
|
assert 'id="modal-detaliu"' in html
|
||||||
assert 'id="trimitere-detaliu"' not in html
|
assert 'id="trimitere-detaliu"' not in html
|
||||||
|
|
||||||
@@ -108,7 +107,7 @@ def test_acasa_fara_linkuri_ajutor(client):
|
|||||||
|
|
||||||
|
|
||||||
def test_acasa_fara_wayfinding_ajutor(client):
|
def test_acasa_fara_wayfinding_ajutor(client):
|
||||||
"""US-001 (5.5): randul 'Ajutor' (wayfinding Mapari/Coduri RAR) eliminat din Acasa —
|
"""Randul 'Ajutor' (wayfinding Mapari/Coduri RAR) eliminat din Acasa —
|
||||||
navigarea traieste in tab-bar si in meniul de cont."""
|
navigarea traieste in tab-bar si in meniul de cont."""
|
||||||
r = client.get("/?tab=acasa")
|
r = client.get("/?tab=acasa")
|
||||||
html = r.text
|
html = r.text
|
||||||
@@ -132,7 +131,7 @@ def test_trimiteri_fara_poll_periodic_pe_tabel(client):
|
|||||||
"""Tabelul de trimiteri NU se reimprospateaza periodic: #submissions-wrap se
|
"""Tabelul de trimiteri NU se reimprospateaza periodic: #submissions-wrap se
|
||||||
incarca la load / actiunile utilizatorului (trimiteriChanged) / reincarcaTrimiteri.
|
incarca la load / actiunile utilizatorului (trimiteriChanged) / reincarcaTrimiteri.
|
||||||
Reimprospatarea automata la date noi externe se face prin pollerul de versiune
|
Reimprospatarea automata la date noi externe se face prin pollerul de versiune
|
||||||
care cheama reincarcaTrimiteri() — fara nudge manual (US-008)."""
|
care cheama reincarcaTrimiteri()."""
|
||||||
_seed_submission("sent")
|
_seed_submission("sent")
|
||||||
r = client.get("/?tab=acasa")
|
r = client.get("/?tab=acasa")
|
||||||
html = r.text
|
html = r.text
|
||||||
@@ -142,6 +141,6 @@ def test_trimiteri_fara_poll_periodic_pe_tabel(client):
|
|||||||
assert "every" not in wrap, f"tabelul nu trebuie sa aiba poll periodic: {wrap}"
|
assert "every" not in wrap, f"tabelul nu trebuie sa aiba poll periodic: {wrap}"
|
||||||
assert "reincarcaTrimiteri" in wrap
|
assert "reincarcaTrimiteri" in wrap
|
||||||
assert "trimiteriChanged" in wrap
|
assert "trimiteriChanged" in wrap
|
||||||
# Pollerul de versiune exista (auto-refresh la date noi externe). Nudge eliminat (US-008).
|
# Pollerul de versiune exista (auto-refresh la date noi externe); fara nudge manual.
|
||||||
assert "/_fragments/trimiteri-versiune" in html
|
assert "/_fragments/trimiteri-versiune" in html
|
||||||
assert 'id="nudge-trimiteri"' not in html
|
assert 'id="nudge-trimiteri"' not in html
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Teste US-004 (PRD 5.5): model de stare a contului `accounts.status` + helperi.
|
"""Teste model de stare a contului `accounts.status` + helperi.
|
||||||
|
|
||||||
Acopera: derivarea status din active la migrare, invariantul active=1 <=> status='active',
|
Acopera: derivarea status din active la migrare, invariantul active=1 <=> status='active',
|
||||||
helperii set_status/delete_account, protectia contului de sistem id=1, excluderea 'deleted'
|
helperii set_status/delete_account, protectia contului de sistem id=1, excluderea 'deleted'
|
||||||
@@ -94,7 +94,7 @@ def test_delete_purjeaza_pii_si_elibereaza_cui(conn):
|
|||||||
"""Stergerea soft purjeaza creds RAR + revoca cheile API + elibereaza CUI (re-inregistrabil)."""
|
"""Stergerea soft purjeaza creds RAR + revoca cheile API + elibereaza CUI (re-inregistrabil)."""
|
||||||
from app.accounts import create_account, delete_account, list_accounts
|
from app.accounts import create_account, delete_account, list_accounts
|
||||||
acct_id = create_account(conn, "Service GDPR", cui="RO12345", active=True)
|
acct_id = create_account(conn, "Service GDPR", cui="RO12345", active=True)
|
||||||
# US-013: creds in sloturi per-env (rar_creds_enc legacy dropata)
|
# creds in sloturi per-env
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"UPDATE accounts SET rar_creds_test_enc='secret_enc_test', rar_creds_prod_enc='secret_enc_prod' WHERE id=?",
|
"UPDATE accounts SET rar_creds_test_enc='secret_enc_test', rar_creds_prod_enc='secret_enc_prod' WHERE id=?",
|
||||||
(acct_id,),
|
(acct_id,),
|
||||||
@@ -138,7 +138,6 @@ def test_migrare_deriva_status_din_active(conn):
|
|||||||
from app.db import _migrate
|
from app.db import _migrate
|
||||||
|
|
||||||
# Reconstruim accounts fara `status` (rebuild de tabela — singura cale in SQLite vechi).
|
# Reconstruim accounts fara `status` (rebuild de tabela — singura cale in SQLite vechi).
|
||||||
# US-013: rar_creds_enc nu mai exista in accounts (dropata); nu e in legacy DDL.
|
|
||||||
conn.executescript(
|
conn.executescript(
|
||||||
"""
|
"""
|
||||||
PRAGMA foreign_keys=OFF;
|
PRAGMA foreign_keys=OFF;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Teste US-001 (PRD 3.1): coloana accounts.active + helper-e cont in app/accounts.py."""
|
"""Teste coloana accounts.active + helper-e cont in app/accounts.py."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -119,7 +119,7 @@ def test_list_accounts_ordonat_fara_creds(conn):
|
|||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# US-001 (PRD 5.12): accounts.email + validari companie/email/CUI
|
# accounts.email + validari companie/email/CUI
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def test_create_account_fara_email_ridica(conn):
|
def test_create_account_fara_email_ridica(conn):
|
||||||
@@ -176,7 +176,7 @@ def test_account_is_complete_false_pe_legacy_incomplet(conn):
|
|||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 5.17 US-001/US-008: schema tier + trial_until + set_tier + CLI set-tier
|
# schema tier + trial_until + set_tier + CLI set-tier
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def test_migrare_tier_trial_defensiva(conn):
|
def test_migrare_tier_trial_defensiva(conn):
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Teste US-008 (PRD 5.5): rute admin pentru ciclul de viata al conturilor —
|
"""Rute admin pentru ciclul de viata al conturilor —
|
||||||
block/archive/delete + bulk pe lista account_id, require_admin + CSRF + PRG, dev id=1 protejat.
|
block/archive/delete + bulk pe lista account_id, require_admin + CSRF + PRG, dev id=1 protejat.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
"""Teste US-011 (PRD 3.3b): panou admin web /admin — conturi in asteptare + activare.
|
"""Teste panou admin web /admin — conturi in asteptare + activare.
|
||||||
|
|
||||||
TDD strict: testele se scriu INAINTE de implementare; la inceput pica (RED),
|
|
||||||
dupa implementare trec (GREEN).
|
|
||||||
|
|
||||||
Fisiere testate: app/web/admin_routes.py, app/web/templates/admin.html.
|
Fisiere testate: app/web/admin_routes.py, app/web/templates/admin.html.
|
||||||
"""
|
"""
|
||||||
@@ -175,7 +172,7 @@ def test_non_admin_403(client):
|
|||||||
Creeaza intai un cont admin (bootstrap: primul user devine admin),
|
Creeaza intai un cont admin (bootstrap: primul user devine admin),
|
||||||
apoi un al doilea cont (non-admin) si verifica ca al doilea primeste 403.
|
apoi un al doilea cont (non-admin) si verifica ca al doilea primeste 403.
|
||||||
"""
|
"""
|
||||||
# Primul signup devine automat admin (bootstrap US-010)
|
# Primul signup devine automat admin (bootstrap)
|
||||||
_signup(client, "Admin Bootstrap SA", "bootstrap@test.ro")
|
_signup(client, "Admin Bootstrap SA", "bootstrap@test.ro")
|
||||||
|
|
||||||
# Al doilea user NU e admin
|
# Al doilea user NU e admin
|
||||||
@@ -286,7 +283,7 @@ def test_set_tier_din_admin_incheie_trial(client):
|
|||||||
"""POST /admin/set-tier -> tier actualizat, trial_until=NULL (trial incheiat), 303.
|
"""POST /admin/set-tier -> tier actualizat, trial_until=NULL (trial incheiat), 303.
|
||||||
|
|
||||||
Contul nou are trial Pro 30z; alocarea manuala trebuie sa-l incheie ca alegerea
|
Contul nou are trial Pro 30z; alocarea manuala trebuie sa-l incheie ca alegerea
|
||||||
sa aiba efect imediat (decizie user 2026-06-29)."""
|
sa aiba efect imediat."""
|
||||||
target_id = _signup(client, "Firma Upgrade SRL", "upgrade@test.ro")
|
target_id = _signup(client, "Firma Upgrade SRL", "upgrade@test.ro")
|
||||||
tier0, trial0 = _get_tier_trial(target_id)
|
tier0, trial0 = _get_tier_trial(target_id)
|
||||||
assert tier0 == "free", "cont nou trebuie sa porneasca pe free"
|
assert tier0 == "free", "cont nou trebuie sa porneasca pe free"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Teste US-010 (PRD 3.3b): rol admin + bootstrap + guard require_admin.
|
"""Teste rol admin + bootstrap + guard require_admin.
|
||||||
|
|
||||||
Fisiere testate: app/users.py (helper-e admin), app/web/session.py (require_admin),
|
Fisiere testate: app/users.py (helper-e admin), app/web/session.py (require_admin),
|
||||||
tools/account.py (subcomanda set-admin).
|
tools/account.py (subcomanda set-admin).
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Teste API /v1/prezentari — rutare validare (T3) + idempotenta."""
|
"""Teste API /v1/prezentari — rutare validare + idempotenta."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ def test_idempotenta_dedup(client):
|
|||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------------- #
|
# --------------------------------------------------------------------------- #
|
||||||
# PRD 5.7 — raspuns onest: erori/nemapate/motiv pe randuri blocate #
|
# Raspuns onest: erori/nemapate/motiv pe randuri blocate #
|
||||||
# --------------------------------------------------------------------------- #
|
# --------------------------------------------------------------------------- #
|
||||||
|
|
||||||
def test_needs_data_intoarce_erori(client):
|
def test_needs_data_intoarce_erori(client):
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Teste US-010 (PRD 5.6): API DELETE + re-pune in coada, scoped + oracol scope/stare."""
|
"""Teste API DELETE + re-pune in coada, scoped + oracol scope/stare."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Teste US-005 — camp rar_env pe POST /v1/prezentari si /valideaza.
|
"""Teste camp rar_env pe POST /v1/prezentari si /valideaza.
|
||||||
|
|
||||||
Acopera: default din cont, tinta explicita, respingere tinta indisponibila,
|
Acopera: default din cont, tinta explicita, respingere tinta indisponibila,
|
||||||
echo GET, valoare invalida (422 Pydantic), echo dry-run valideaza.
|
echo GET, valoare invalida (422 Pydantic), echo dry-run valideaza.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""US-011 (PRD 5.15): account-scope pe GET-urile de listare API (securitate).
|
"""Teste account-scope pe GET-urile de listare API (securitate).
|
||||||
|
|
||||||
Verifica:
|
Verifica:
|
||||||
- GET /v1/prezentari: un cont nu vede submissions ale altui cont
|
- GET /v1/prezentari: un cont nu vede submissions ale altui cont
|
||||||
@@ -6,9 +6,8 @@ Verifica:
|
|||||||
- Unauthenticated access cu require_api_key=True -> 401
|
- Unauthenticated access cu require_api_key=True -> 401
|
||||||
- Filtre si paginare nu sparg scope-ul
|
- Filtre si paginare nu sparg scope-ul
|
||||||
|
|
||||||
Legatura cu implementare: resolve_account_id + account_scope_clause (mecanisme
|
Foloseste resolve_account_id + account_scope_clause; verifica alinierea
|
||||||
existente, reutilizate). Testele LOCK DOWN comportamentul deja implementat
|
cu AUTOPASS_REQUIRE_API_KEY (dev vs prod).
|
||||||
si verifica alinierea cu AUTOPASS_REQUIRE_API_KEY (dev vs prod).
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -227,7 +226,7 @@ def test_get_listare_filtru_status_nu_sparge_scope(client):
|
|||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# PRD 5.17 — enforcement planuri: gate API (T4) + volum (T3) + kill-switch (T5)
|
# Enforcement planuri: gate API + volum + kill-switch
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
_PREZ_PLAN = {
|
_PREZ_PLAN = {
|
||||||
@@ -279,7 +278,7 @@ def _insert_n_submissions(account_id: int, n: int) -> None:
|
|||||||
def test_free_fara_api_403(client):
|
def test_free_fara_api_403(client):
|
||||||
"""Cont free (non-default, cu cheie API) → 403 PLAN_FARA_API pe POST /v1/prezentari.
|
"""Cont free (non-default, cu cheie API) → 403 PLAN_FARA_API pe POST /v1/prezentari.
|
||||||
|
|
||||||
T4 PRD 5.17: gate API refuza conturi cu api_access=False in PLANS.
|
Gate-ul API refuza conturi cu api_access=False in PLANS.
|
||||||
"""
|
"""
|
||||||
acct_id, key = _create_account_with_key("FreePlanGate")
|
acct_id, key = _create_account_with_key("FreePlanGate")
|
||||||
_set_tier_acct(acct_id, "free", trial_until=None)
|
_set_tier_acct(acct_id, "free", trial_until=None)
|
||||||
@@ -349,8 +348,8 @@ def test_trial_pro_api_ok(client):
|
|||||||
def test_dry_run_valideaza_ramane_permis(client):
|
def test_dry_run_valideaza_ramane_permis(client):
|
||||||
"""POST /v1/prezentari/valideaza (dry-run) e permis pe orice plan, inclusiv free.
|
"""POST /v1/prezentari/valideaza (dry-run) e permis pe orice plan, inclusiv free.
|
||||||
|
|
||||||
Decizie design (PRD 5.17): valideaza nu face enqueue si nu consuma cota,
|
Valideaza nu face enqueue si nu consuma cota, deci NU e protejat de
|
||||||
deci NU e protejat de gate-ul API — integratorii pot testa fara upgrade.
|
gate-ul API — integratorii pot testa fara upgrade.
|
||||||
"""
|
"""
|
||||||
acct_id, key = _create_account_with_key("FreeValideazaTest")
|
acct_id, key = _create_account_with_key("FreeValideazaTest")
|
||||||
_set_tier_acct(acct_id, "free", trial_until=None)
|
_set_tier_acct(acct_id, "free", trial_until=None)
|
||||||
@@ -374,8 +373,8 @@ def test_dry_run_valideaza_ramane_permis(client):
|
|||||||
def test_free_peste_60_respins_api(client):
|
def test_free_peste_60_respins_api(client):
|
||||||
"""Dev account (id=1) la 60/60 pe free → a 61-a prezentare respinsa 422 PLAN_LIMITA_LUNARA.
|
"""Dev account (id=1) la 60/60 pe free → a 61-a prezentare respinsa 422 PLAN_LIMITA_LUNARA.
|
||||||
|
|
||||||
Dev account (id=1) in dev mode (require_api_key=False) are bypass la gate-ul API (T4),
|
Dev account (id=1) in dev mode (require_api_key=False) are bypass la gate-ul API,
|
||||||
dar NU la verificarea de volum (T3). La 60/60, urmatoarea cerere trebuie respinsa 422.
|
dar NU la verificarea de volum. La 60/60, urmatoarea cerere trebuie respinsa 422.
|
||||||
"""
|
"""
|
||||||
_set_tier_acct(1, "free", trial_until=None)
|
_set_tier_acct(1, "free", trial_until=None)
|
||||||
_insert_n_submissions(1, 60)
|
_insert_n_submissions(1, 60)
|
||||||
@@ -392,7 +391,7 @@ def test_free_peste_60_respins_api(client):
|
|||||||
def test_eroare_3_niveluri_plan_limita(client):
|
def test_eroare_3_niveluri_plan_limita(client):
|
||||||
"""Eroarea PLAN_LIMITA_LUNARA contine toate 3 nivelurile: cod, problema, cauza, fix.
|
"""Eroarea PLAN_LIMITA_LUNARA contine toate 3 nivelurile: cod, problema, cauza, fix.
|
||||||
|
|
||||||
Pattern standard (PRD 5.17): eroare structurata pe 3 niveluri — nu 500, nu catch-all.
|
Pattern standard de eroare structurata pe 3 niveluri — nu 500, nu catch-all.
|
||||||
"""
|
"""
|
||||||
_set_tier_acct(1, "free", trial_until=None)
|
_set_tier_acct(1, "free", trial_until=None)
|
||||||
_insert_n_submissions(1, 60)
|
_insert_n_submissions(1, 60)
|
||||||
@@ -512,7 +511,7 @@ def test_dev_id1_neblocat(client):
|
|||||||
def test_flag_enforce_plans_false_sare_enforcement(client, monkeypatch):
|
def test_flag_enforce_plans_false_sare_enforcement(client, monkeypatch):
|
||||||
"""Kill-switch AUTOPASS_ENFORCE_PLANS=false sare toate gate-urile de plan.
|
"""Kill-switch AUTOPASS_ENFORCE_PLANS=false sare toate gate-urile de plan.
|
||||||
|
|
||||||
T5 PRD 5.17: flag de operare pentru debugging sau rollback rapid fara revert de cod.
|
Flag de operare pentru debugging sau rollback rapid fara revert de cod.
|
||||||
Chiar si cu free la 60/60, nu trebuie 422 cand flag-ul e oprit.
|
Chiar si cu free la 60/60, nu trebuie 422 cand flag-ul e oprit.
|
||||||
"""
|
"""
|
||||||
from app.config import get_settings
|
from app.config import get_settings
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Teste US-004 (PRD 5.6): audit cerere API per cont in jurnal."""
|
"""Teste audit cerere API per cont in jurnal."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""US-001 (PRD 5.19): schema comutator Auto per cont + flag `held` pe submission.
|
"""Schema comutator Auto per cont + flag `held` pe submission.
|
||||||
|
|
||||||
Acopera: migrarea aditiva idempotenta (coloane + index partial pe DB vechi), helperii
|
Acopera: migrarea aditiva idempotenta (coloane + index partial pe DB vechi), helperii
|
||||||
`get_auto_send`/`set_auto_send`/`held_for_account` din app/accounts.py si default-ul 0.
|
`get_auto_send`/`set_auto_send`/`held_for_account` din app/accounts.py si default-ul 0.
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
"""Teste US-007 (PRD 3.6) actualizate dupa US-002 (PRD 5.11).
|
"""Teste comutator auto_send: macro autosend_toggle neutralizat (intoarce string gol).
|
||||||
|
|
||||||
US-002: macro autosend_toggle neutralizat (intoarce string gol).
|
|
||||||
Checkbox-ul name=auto_send a fost scos din UI. Coloanele DB raman.
|
Checkbox-ul name=auto_send a fost scos din UI. Coloanele DB raman.
|
||||||
Testele de UI verifica ABSENTA toggle-ului; testele de backend (stocare DB) raman.
|
Testele de UI verifica ABSENTA toggle-ului; testele de backend (stocare DB) raman.
|
||||||
"""
|
"""
|
||||||
@@ -97,24 +96,24 @@ def _macro_html(checked: bool = True, form_id: str = "") -> str:
|
|||||||
return str(mod.autosend_toggle(form_id=form_id, checked=checked))
|
return str(mod.autosend_toggle(form_id=form_id, checked=checked))
|
||||||
|
|
||||||
|
|
||||||
# --- markup: macro neutralizat dupa US-002 ---
|
# --- markup: macro neutralizat ---
|
||||||
|
|
||||||
def test_comutator_coada_prezent():
|
def test_comutator_coada_prezent():
|
||||||
"""US-002: macro autosend_toggle neutralizat -> output gol (fara checkbox)."""
|
"""macro autosend_toggle neutralizat -> output gol (fara checkbox)."""
|
||||||
html = _macro_html()
|
html = _macro_html()
|
||||||
assert 'name="auto_send"' not in html, "US-002: checkbox auto_send scos din UI"
|
assert 'name="auto_send"' not in html, "checkbox auto_send scos din UI"
|
||||||
assert html.strip() == "", f"macro neutralizat trebuie sa intoarca string gol, got: {html!r}"
|
assert html.strip() == "", f"macro neutralizat trebuie sa intoarca string gol, got: {html!r}"
|
||||||
|
|
||||||
|
|
||||||
def test_eticheta_scoped_pe_operatie():
|
def test_eticheta_scoped_pe_operatie():
|
||||||
"""US-002: macro neutralizat -> nicio eticheta scoped."""
|
"""macro neutralizat -> nicio eticheta scoped."""
|
||||||
html = _macro_html()
|
html = _macro_html()
|
||||||
assert "aceasta operatie" not in html
|
assert "aceasta operatie" not in html
|
||||||
assert html.strip() == ""
|
assert html.strip() == ""
|
||||||
|
|
||||||
|
|
||||||
def test_default_pune_automat():
|
def test_default_pune_automat():
|
||||||
"""US-002: macro neutralizat intoarce gol indiferent de parametrul checked."""
|
"""macro neutralizat intoarce gol indiferent de parametrul checked."""
|
||||||
html_default = _macro_html(checked=True)
|
html_default = _macro_html(checked=True)
|
||||||
assert html_default.strip() == ""
|
assert html_default.strip() == ""
|
||||||
html_off = _macro_html(checked=False)
|
html_off = _macro_html(checked=False)
|
||||||
@@ -154,7 +153,7 @@ def test_tine_pentru_verificare_mapeaza_auto_send_false(client):
|
|||||||
# --- prezent in AMBELE locuri (mapari tab + panou preview) ---
|
# --- prezent in AMBELE locuri (mapari tab + panou preview) ---
|
||||||
|
|
||||||
def test_comutator_in_tab_mapari(client):
|
def test_comutator_in_tab_mapari(client):
|
||||||
"""US-002: tabul Mapari nu mai contine checkbox auto_send (macro neutralizat)."""
|
"""Tabul Mapari nu mai contine checkbox auto_send (macro neutralizat)."""
|
||||||
from app.db import get_connection
|
from app.db import get_connection
|
||||||
import json
|
import json
|
||||||
acct = _create_account_user("tm@test.com")
|
acct = _create_account_user("tm@test.com")
|
||||||
@@ -172,12 +171,12 @@ def test_comutator_in_tab_mapari(client):
|
|||||||
_login(client, "tm@test.com")
|
_login(client, "tm@test.com")
|
||||||
resp = client.get("/?tab=mapari")
|
resp = client.get("/?tab=mapari")
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
assert 'name="auto_send"' not in resp.text, "US-002: checkbox auto_send scos din UI"
|
assert 'name="auto_send"' not in resp.text, "checkbox auto_send scos din UI"
|
||||||
assert "In coada" not in resp.text, "US-002: coloana 'In coada' scoasa"
|
assert "In coada" not in resp.text, "coloana 'In coada' scoasa"
|
||||||
|
|
||||||
|
|
||||||
def test_comutator_in_panou_preview(client):
|
def test_comutator_in_panou_preview(client):
|
||||||
"""US-002: panoul de mapare din preview nu mai contine checkbox auto_send."""
|
"""Panoul de mapare din preview nu mai contine checkbox auto_send."""
|
||||||
_create_account_user("pp@test.com")
|
_create_account_user("pp@test.com")
|
||||||
_seed_nomenclator("R-FRANE")
|
_seed_nomenclator("R-FRANE")
|
||||||
_login(client, "pp@test.com")
|
_login(client, "pp@test.com")
|
||||||
@@ -203,5 +202,5 @@ def test_comutator_in_panou_preview(client):
|
|||||||
})
|
})
|
||||||
assert r.status_code == 200
|
assert r.status_code == 200
|
||||||
assert "OP-NEMAPAT" in r.text, "operatia nemapata trebuie sa apara in panoul de mapare"
|
assert "OP-NEMAPAT" in r.text, "operatia nemapata trebuie sa apara in panoul de mapare"
|
||||||
assert 'name="auto_send"' not in r.text, "US-002: checkbox auto_send scos din preview"
|
assert 'name="auto_send"' not in r.text, "checkbox auto_send scos din preview"
|
||||||
assert "In coada automat" not in r.text, "US-002: eticheta 'In coada automat' scoasa"
|
assert "In coada automat" not in r.text, "eticheta 'In coada automat' scoasa"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Teste US-010 (PRD 5.20): badge mediu RAR in liste/detaliu/audit/ecou API.
|
"""Teste badge mediu RAR in liste/detaliu/audit/ecou API.
|
||||||
|
|
||||||
Teste:
|
Teste:
|
||||||
test_badge_in_lista -- /_fragments/submissions afiseaza PRODUCȚIE / Testare per rand
|
test_badge_in_lista -- /_fragments/submissions afiseaza PRODUCȚIE / Testare per rand
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ def test_legacy_dual_lookup_strategy():
|
|||||||
assert found, "dual-lookup trebuie sa gaseasca randul vechi"
|
assert found, "dual-lookup trebuie sa gaseasca randul vechi"
|
||||||
|
|
||||||
|
|
||||||
# --- Integrare: API route foloseste build_key (OV-2) ---
|
# --- Integrare: API route foloseste build_key ---
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def client(monkeypatch):
|
def client(monkeypatch):
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Teste PRD 5.19 R4 — tools/carantina_held.py (atenuare hazard de rollback).
|
"""Teste tools/carantina_held.py (atenuare hazard de rollback).
|
||||||
|
|
||||||
Carantineaza randurile `queued AND held=1` (-> `error`/`ROLLBACK_QUARANTINE`)
|
Carantineaza randurile `queued AND held=1` (-> `error`/`ROLLBACK_QUARANTINE`)
|
||||||
ca sa nu fie trimise de un worker fara filtrul `AND held=0` dupa un revert.
|
ca sa nu fie trimise de un worker fara filtrul `AND held=0` dupa un revert.
|
||||||
@@ -63,7 +63,7 @@ def test_carantineaza_rand_tinut(env, capsys):
|
|||||||
r = _row(conn, sid)
|
r = _row(conn, sid)
|
||||||
assert r["status"] == "error"
|
assert r["status"] == "error"
|
||||||
assert r["rar_error"] == "ROLLBACK_QUARANTINE"
|
assert r["rar_error"] == "ROLLBACK_QUARANTINE"
|
||||||
# /code-review #4: randul carantinat primeste purge_after (altfel PII ar sta la nesfarsit,
|
# randul carantinat primeste purge_after (altfel PII ar sta la nesfarsit,
|
||||||
# purge_expired sterge doar randuri cu purge_after setat).
|
# purge_expired sterge doar randuri cu purge_after setat).
|
||||||
assert r["purge_after"] is not None
|
assert r["purge_after"] is not None
|
||||||
assert conn.execute(
|
assert conn.execute(
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Teste US-008 (PRD 5.20): configurare medii RAR per cont — Testare + Productie.
|
"""Teste configurare medii RAR per cont — Testare + Productie.
|
||||||
|
|
||||||
Ruta testata: POST /cont/rar-medii
|
Ruta testata: POST /cont/rar-medii
|
||||||
|
|
||||||
|
|||||||
@@ -47,12 +47,12 @@ def _body(**over):
|
|||||||
def test_dashboard_renders_with_rar_state(client):
|
def test_dashboard_renders_with_rar_state(client):
|
||||||
r = client.get("/")
|
r = client.get("/")
|
||||||
assert r.status_code == 200
|
assert r.status_code == 200
|
||||||
# Dupa US-003 bara de status e incarcata via HTMX (hx-trigger=load, every 15s)
|
# Bara de status e incarcata via HTMX (hx-trigger=load, every 15s)
|
||||||
assert "/_fragments/status" in r.text, "Dashboard-ul trebuie sa referenceze fragmentul de status"
|
assert "/_fragments/status" in r.text, "Dashboard-ul trebuie sa referenceze fragmentul de status"
|
||||||
# Fragmentul de status contine starea de sanatate (text uman, nu brut tehnic)
|
# Fragmentul de status contine starea de sanatate (text uman, nu brut tehnic)
|
||||||
rs = client.get("/_fragments/status")
|
rs = client.get("/_fragments/status")
|
||||||
assert rs.status_code == 200
|
assert rs.status_code == 200
|
||||||
# US-003 D6: strip sanatate unificat — "declaratiile" apare in orice stare (curg/blocat)
|
# strip sanatate unificat — "declaratiile" apare in orice stare (curg/blocat)
|
||||||
assert "declaratiile" in rs.text.lower(), (
|
assert "declaratiile" in rs.text.lower(), (
|
||||||
f"Strip sanatate lipseste din fragment. HTML: {rs.text[:500]}"
|
f"Strip sanatate lipseste din fragment. HTML: {rs.text[:500]}"
|
||||||
)
|
)
|
||||||
@@ -73,7 +73,7 @@ def test_nomenclator_fragment_lists_seed(client):
|
|||||||
def test_submissions_fragment_empty_state(client):
|
def test_submissions_fragment_empty_state(client):
|
||||||
r = client.get("/_fragments/submissions")
|
r = client.get("/_fragments/submissions")
|
||||||
assert r.status_code == 200
|
assert r.status_code == 200
|
||||||
# US-005: empty state prietenos cu indemn la Import (nu mesajul tehnic vechi)
|
# empty state prietenos cu indemn la Import (nu mesajul tehnic vechi)
|
||||||
assert "Nicio trimitere" in r.text or "incepe cu Import" in r.text or "?tab=import" in r.text
|
assert "Nicio trimitere" in r.text or "incepe cu Import" in r.text or "?tab=import" in r.text
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
"""Test US-011 (discoverability): linkul 'Panou admin' apare pe dashboard doar pentru admini.
|
"""Linkul 'Panou admin' apare pe dashboard doar pentru admini.
|
||||||
|
|
||||||
Completeaza intentia US-011 — adminul trebuie sa poata descoperi /admin din UI, nu doar
|
Adminul trebuie sa poata descoperi /admin din UI, nu doar prin URL direct.
|
||||||
prin URL direct.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""Teste US-005 (PRD 3.3): scoping dashboard pe sesiune (2 conturi, citiri).
|
"""Teste scoping dashboard pe sesiune (2 conturi, citiri).
|
||||||
|
|
||||||
Comportamental (C6): nu grep, ci verificare reala cu 2 conturi + date distincte.
|
Verificare comportamentala reala cu 2 conturi + date distincte, nu grep.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Teste US-012 (PRD 5.6): un rand `error` nu mai blocheaza retrimiterea (dedup).
|
"""Un rand `error` nu mai blocheaza retrimiterea (dedup).
|
||||||
|
|
||||||
La resubmit cu aceeasi cheie de continut peste un rand `error`: se RE-ACTIVEAZA
|
La resubmit cu aceeasi cheie de continut peste un rand `error`: se RE-ACTIVEAZA
|
||||||
(reactivated:true + stare noua), creds-urile se actualizeaza. Peste sent/queued/
|
(reactivated:true + stare noua), creds-urile se actualizeaza. Peste sent/queued/
|
||||||
@@ -82,7 +82,7 @@ def test_resubmit_actualizeaza_creds_pe_reactivare(client):
|
|||||||
enc_dupa = _creds_enc(sid)
|
enc_dupa = _creds_enc(sid)
|
||||||
assert enc_dupa is not None
|
assert enc_dupa is not None
|
||||||
assert enc_dupa != enc_initial, "creds-urile trebuie actualizate la reactivare"
|
assert enc_dupa != enc_initial, "creds-urile trebuie actualizate la reactivare"
|
||||||
# US-013: creds propagate in slotul per-env (rar_creds_test_enc, ancora globala=test)
|
# creds propagate in slotul per-env (rar_creds_test_enc, ancora globala=test)
|
||||||
from app.db import get_connection
|
from app.db import get_connection
|
||||||
from app.crypto import decrypt_creds
|
from app.crypto import decrypt_creds
|
||||||
conn = get_connection()
|
conn = get_connection()
|
||||||
@@ -118,7 +118,7 @@ def test_resubmit_peste_queued_ramane_deduped(client):
|
|||||||
|
|
||||||
|
|
||||||
def test_reactivare_pe_needs_data_expune_erori(client):
|
def test_reactivare_pe_needs_data_expune_erori(client):
|
||||||
"""PRD 5.7: reactivarea unui rand error care re-clasifica needs_data trebuie sa
|
"""Reactivarea unui rand error care re-clasifica needs_data trebuie sa
|
||||||
expuna erori/motiv (raspuns onest), nu doar status + reactivated."""
|
expuna erori/motiv (raspuns onest), nu doar status + reactivated."""
|
||||||
# rand initial valid -> queued; il fortam error
|
# rand initial valid -> queued; il fortam error
|
||||||
r1 = client.post("/v1/prezentari", json=_body())
|
r1 = client.post("/v1/prezentari", json=_body())
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
"""Teste US-012 (PRD 5.15): Analytics device-mix — validare premisa mobil, fara PII.
|
"""Teste analytics device-mix — validare premisa mobil, fara PII.
|
||||||
|
|
||||||
TDD: RED inainte de implementare.
|
|
||||||
|
|
||||||
Semnal: la acces dashboard -> eveniment 'device_mix' in app_events cu cod 'desktop'/'mobil'.
|
Semnal: la acces dashboard -> eveniment 'device_mix' in app_events cu cod 'desktop'/'mobil'.
|
||||||
Zero PII: nu se stocheaza UA brut, IP sau VIN.
|
Zero PII: nu se stocheaza UA brut, IP sau VIN.
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
"""Teste US-012 (PRD 5.20): audit evenimente medii RAR + e2e lant submission.
|
"""Teste audit evenimente medii RAR + e2e lant submission.
|
||||||
|
|
||||||
Fisier nou; nu modifica teste existente.
|
|
||||||
|
|
||||||
Teste:
|
Teste:
|
||||||
test_lant_import_pana_la_queued -- submission ajunge la status=queued cu rar_env corect
|
test_lant_import_pana_la_queued -- submission ajunge la status=queued cu rar_env corect
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""US-005 (PRD 5.18) — embeddings indexeaza corpusul etichetat (NU nomenclatorul).
|
"""Embeddings indexeaza corpusul etichetat (NU nomenclatorul).
|
||||||
|
|
||||||
k-NN peste exemple reale etichetate (denumire_normalizata -> cod, is_nul) e net mai
|
k-NN peste exemple reale etichetate (denumire_normalizata -> cod, is_nul) e net mai
|
||||||
precis decat peste cele 18 categorii generice. Acopera si simetria corpus/query (F1):
|
precis decat peste cele 18 categorii generice. Acopera si simetria corpus/query:
|
||||||
corpusul e text NORMALIZAT, deci query-ul trebuie normalizat la fel inainte de embedding.
|
corpusul e text NORMALIZAT, deci query-ul trebuie normalizat la fel inainte de embedding.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ def env(monkeypatch):
|
|||||||
tmp = tempfile.mkdtemp()
|
tmp = tempfile.mkdtemp()
|
||||||
monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "us005.db"))
|
monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "us005.db"))
|
||||||
monkeypatch.setenv("AUTOPASS_WEB_AUTH_REQUIRED", "false")
|
monkeypatch.setenv("AUTOPASS_WEB_AUTH_REQUIRED", "false")
|
||||||
monkeypatch.setenv("AUTOPASS_EMBEDDINGS_ENABLED", "true") # US-005 are nevoie de embeddings ON
|
monkeypatch.setenv("AUTOPASS_EMBEDDINGS_ENABLED", "true") # necesar pentru testele de embeddings
|
||||||
from app.config import get_settings
|
from app.config import get_settings
|
||||||
get_settings.cache_clear()
|
get_settings.cache_clear()
|
||||||
from app.db import init_db
|
from app.db import init_db
|
||||||
@@ -115,7 +115,7 @@ def test_semnatura_corpus_pe_seed(conn):
|
|||||||
|
|
||||||
|
|
||||||
def test_query_normalizat_ca_si_corpusul(conn, monkeypatch):
|
def test_query_normalizat_ca_si_corpusul(conn, monkeypatch):
|
||||||
"""F1 (HIGH): enrich_suggestions interogheaza suggest_nearest cu textul NORMALIZAT."""
|
"""enrich_suggestions interogheaza suggest_nearest cu textul NORMALIZAT."""
|
||||||
import app.embeddings as emb
|
import app.embeddings as emb
|
||||||
captura = {}
|
captura = {}
|
||||||
monkeypatch.setattr(emb, "has_corpus", lambda: True)
|
monkeypatch.setattr(emb, "has_corpus", lambda: True)
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
"""US-006 (PRD 5.18) — enrich_suggestions = pre-filtru NUL + k-NN pe corpus etichetat.
|
"""enrich_suggestions = pre-filtru NUL + k-NN pe corpus etichetat.
|
||||||
|
|
||||||
Ordinea de precedenta: pre-filtru NUL -> (daca NUL: fara cod) altfel GOLD partajat >
|
Ordinea de precedenta: pre-filtru NUL -> (daca NUL: fara cod) altfel GOLD partajat >
|
||||||
exact (SILVER) > k-NN embeddings. k-NN sub prag -> abtinere. Vecin k-NN NUL -> supresie.
|
exact (SILVER) > k-NN embeddings. k-NN sub prag -> abtinere. Vecin k-NN NUL -> supresie.
|
||||||
Invariant #13: nimic din asta nu intra in resolve_prestatii/load_mapping.
|
Nimic din enrich_suggestions nu intra in resolve_prestatii/load_mapping.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -123,7 +123,7 @@ def test_vecin_knn_nul_supreseaza(conn, monkeypatch):
|
|||||||
|
|
||||||
|
|
||||||
def test_invariant_13_resolve_neatins(conn):
|
def test_invariant_13_resolve_neatins(conn):
|
||||||
"""Regresie #13: SILVER populat NU produce auto-rezolvare in resolve_prestatii."""
|
"""SILVER populat NU produce auto-rezolvare in resolve_prestatii."""
|
||||||
from app.mapping import resolve_prestatii, normalize_for_match
|
from app.mapping import resolve_prestatii, normalize_for_match
|
||||||
_silver(conn, normalize_for_match("OPERATIE X"), "OE-1")
|
_silver(conn, normalize_for_match("OPERATIE X"), "OE-1")
|
||||||
resolved, unmapped = resolve_prestatii(
|
resolved, unmapped = resolve_prestatii(
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Teste US-001 (PRD 5.6): handler global de excepții -> 500 structurat (3 niveluri) + log."""
|
"""Teste handler global de exceptii -> 500 structurat (3 niveluri) + log."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ def test_exceptie_neasteptata_da_500_structurat(client, monkeypatch):
|
|||||||
assert r.status_code == 500
|
assert r.status_code == 500
|
||||||
body = r.json()
|
body = r.json()
|
||||||
assert body["cod"] == "EROARE_INTERNA"
|
assert body["cod"] == "EROARE_INTERNA"
|
||||||
# 3 niveluri (PRD 5.4): problema + fix
|
# 3 niveluri: cod + problema + fix
|
||||||
assert body["problema"]
|
assert body["problema"]
|
||||||
assert body["fix"]
|
assert body["fix"]
|
||||||
assert "request_id" in body
|
assert "request_id" in body
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
"""Teste pentru app/errors.py — catalog central de erori (US-001 / PRD 5.4).
|
"""Teste pentru app/errors.py — catalog central de erori."""
|
||||||
|
|
||||||
Urmeaza TDD: testele sunt scrise INAINTE de implementare.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""US-002 (PRD 5.18) — etichetator offline multi-backend cu prompt procedural.
|
"""Etichetator offline multi-backend cu prompt procedural.
|
||||||
|
|
||||||
Toate testele ruleaza FARA retea reala (transport injectabil / inspectie body).
|
Toate testele ruleaza FARA retea reala (transport injectabil / inspectie body).
|
||||||
Acopera: prompt 3 pasi, envelope json_schema strict + enum, backend selectabil
|
Acopera: prompt 3 pasi, envelope json_schema strict + enum, backend selectabil
|
||||||
@@ -70,7 +70,7 @@ def test_parseaza_raspuns_si_garda_truncare():
|
|||||||
|
|
||||||
|
|
||||||
def test_backend_selectabil_env(monkeypatch):
|
def test_backend_selectabil_env(monkeypatch):
|
||||||
# Default = lmstudio (backend aprobat v1, D4).
|
# Default = lmstudio.
|
||||||
monkeypatch.delenv("ETICHETARE_BACKEND", raising=False)
|
monkeypatch.delenv("ETICHETARE_BACKEND", raising=False)
|
||||||
assert eticheteaza.get_backend().name == "lmstudio"
|
assert eticheteaza.get_backend().name == "lmstudio"
|
||||||
# Selectie prin env.
|
# Selectie prin env.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Teste US-006 (PRD 3.6): "Formate de coloane salvate" randate ca TABEL.
|
"""Teste "Formate de coloane salvate" randate ca TABEL.
|
||||||
|
|
||||||
Doar marcaj de template — POST-urile /formate-coloane/editeaza si /formate-coloane/sterge
|
Doar marcaj de template — POST-urile /formate-coloane/editeaza si /formate-coloane/sterge
|
||||||
raman identice. .tablewrap pentru consistenta cu celelalte tabele.
|
raman identice. .tablewrap pentru consistenta cu celelalte tabele.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Teste FOUNDATION (Task #1): schema + migrari idempotente + openpyxl disponibil."""
|
"""Teste schema + migrari idempotente + openpyxl disponibil."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -35,11 +35,11 @@ def _tables(conn) -> set[str]:
|
|||||||
# --- Coloane noi ---
|
# --- Coloane noi ---
|
||||||
|
|
||||||
def test_accounts_rar_creds_enc_dropata(db_conn):
|
def test_accounts_rar_creds_enc_dropata(db_conn):
|
||||||
"""US-013: accounts.rar_creds_enc a fost dropata; submissions.rar_creds_enc ramane."""
|
"""accounts.rar_creds_enc a fost dropata; submissions.rar_creds_enc ramane."""
|
||||||
acc_cols = _table_cols(db_conn, "accounts")
|
acc_cols = _table_cols(db_conn, "accounts")
|
||||||
sub_cols = _table_cols(db_conn, "submissions")
|
sub_cols = _table_cols(db_conn, "submissions")
|
||||||
assert "rar_creds_enc" not in acc_cols, \
|
assert "rar_creds_enc" not in acc_cols, \
|
||||||
"accounts.rar_creds_enc trebuie sa fie ABSENTA dupa US-013 DROP"
|
"accounts.rar_creds_enc trebuie sa fie ABSENTA"
|
||||||
assert "rar_creds_enc" in sub_cols, \
|
assert "rar_creds_enc" in sub_cols, \
|
||||||
"submissions.rar_creds_enc trebuie sa RAMANA (creds efemere per-cerere)"
|
"submissions.rar_creds_enc trebuie sa RAMANA (creds efemere per-cerere)"
|
||||||
|
|
||||||
@@ -158,7 +158,7 @@ def test_migrate_on_existing_db(monkeypatch):
|
|||||||
assert "batch_id" in sub_cols
|
assert "batch_id" in sub_cols
|
||||||
assert "row_index" in sub_cols
|
assert "row_index" in sub_cols
|
||||||
assert "purge_after" in sub_cols
|
assert "purge_after" in sub_cols
|
||||||
# US-013: accounts.rar_creds_enc a fost dropata.
|
# accounts.rar_creds_enc a fost dropata.
|
||||||
assert "rar_creds_enc" not in acc_cols
|
assert "rar_creds_enc" not in acc_cols
|
||||||
conn.close()
|
conn.close()
|
||||||
get_settings.cache_clear()
|
get_settings.cache_clear()
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
"""US-003 (PRD 5.18) — generare seed etichetat in faze pe frecventa.
|
"""Generare seed etichetat in faze pe frecventa.
|
||||||
|
|
||||||
Pipeline dedup OBLIGATORIU inainte de orice apel LLM (D5):
|
Pipeline dedup OBLIGATORIU inainte de orice apel LLM:
|
||||||
brut -> normalize_for_match -> arunca chei vide -> dedup pe cheie (freq=suma NR)
|
brut -> normalize_for_match -> arunca chei vide -> dedup pe cheie (freq=suma NR)
|
||||||
-> reuse etichete existente (labels-groq + seed comis, conflict freq-max) -> de_etichetat.
|
-> reuse etichete existente (labels-groq + seed comis, conflict freq-max) -> de_etichetat.
|
||||||
|
|
||||||
Idempotenta cross-run (F2/F7): a doua rulare consuma seedul comis ca cache -> 0 apeluri LLM.
|
Idempotenta cross-run: a doua rulare consuma seedul comis ca cache -> 0 apeluri LLM.
|
||||||
Toate testele FARA retea: `clasifica` e injectat (mock care inregistreaza ce primeste).
|
Toate testele FARA retea: `clasifica` e injectat (mock care inregistreaza ce primeste).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -134,7 +134,7 @@ def test_zero_duplicate_trimis_la_llm(tmp_path):
|
|||||||
|
|
||||||
|
|
||||||
def test_rerun_zero_apeluri_llm(tmp_path):
|
def test_rerun_zero_apeluri_llm(tmp_path):
|
||||||
"""Criteriul real de idempotenta (F2/F7): a doua rulare = 0 apeluri LLM, seed identic."""
|
"""Criteriul real de idempotenta: a doua rulare = 0 apeluri LLM, seed identic."""
|
||||||
f = tmp_path / "a.csv"
|
f = tmp_path / "a.csv"
|
||||||
_scrie_csv(f, [("OP UNU", 10), ("OP DOI", 5)])
|
_scrie_csv(f, [("OP UNU", 10), ("OP DOI", 5)])
|
||||||
seed = tmp_path / "seed.json"
|
seed = tmp_path / "seed.json"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Teste scope cont pe GET /v1/audit/export (US-003, PRD 3.2)."""
|
"""Teste scope cont pe GET /v1/audit/export."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ def test_export_doar_contul_cheii(env):
|
|||||||
from app.db import get_connection
|
from app.db import get_connection
|
||||||
conn = get_connection()
|
conn = get_connection()
|
||||||
try:
|
try:
|
||||||
# tier='pro' ca sa treaca gate-ul API (T4 PRD 5.17); testul masoara scoping, nu planuri.
|
# tier='pro' ca sa treaca gate-ul API; testul masoara scoping, nu planuri.
|
||||||
conn.execute("INSERT INTO accounts (id, name, tier) VALUES (2, 'al-doilea', 'pro')")
|
conn.execute("INSERT INTO accounts (id, name, tier) VALUES (2, 'al-doilea', 'pro')")
|
||||||
k1 = create_api_key(conn, 1)
|
k1 = create_api_key(conn, 1)
|
||||||
k2 = create_api_key(conn, 2)
|
k2 = create_api_key(conn, 2)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""Teste scope cont pe GET /v1/mapari + /pending (US-002, PRD 3.2).
|
"""Teste scope cont pe GET /v1/mapari + /pending.
|
||||||
|
|
||||||
TD-3.2 (decis la poarta): parametrul ?account_id= din query se pastreaza DAR:
|
Parametrul ?account_id= din query se pastreaza DAR:
|
||||||
- daca e prezent SI difera de contul cheii -> 400 explicit
|
- daca e prezent SI difera de contul cheii -> 400 explicit
|
||||||
- daca e prezent si egal -> ok
|
- daca e prezent si egal -> ok
|
||||||
- daca lipseste -> contul cheii
|
- daca lipseste -> contul cheii
|
||||||
@@ -69,7 +69,7 @@ def test_mapari_doar_contul_cheii(env):
|
|||||||
|
|
||||||
|
|
||||||
def test_mapari_query_account_id_diferit_400(env):
|
def test_mapari_query_account_id_diferit_400(env):
|
||||||
"""Daca ?account_id difera de contul cheii -> 400 explicit (TD-3.2)."""
|
"""Daca ?account_id difera de contul cheii -> 400 explicit."""
|
||||||
with _client() as c:
|
with _client() as c:
|
||||||
from app.auth import create_api_key
|
from app.auth import create_api_key
|
||||||
from app.db import get_connection
|
from app.db import get_connection
|
||||||
@@ -85,7 +85,7 @@ def test_mapari_query_account_id_diferit_400(env):
|
|||||||
|
|
||||||
|
|
||||||
def test_mapari_query_account_id_egal_ok(env):
|
def test_mapari_query_account_id_egal_ok(env):
|
||||||
"""Daca ?account_id egal cu contul cheii -> 200 (TD-3.2)."""
|
"""Daca ?account_id egal cu contul cheii -> 200."""
|
||||||
with _client() as c:
|
with _client() as c:
|
||||||
from app.auth import create_api_key
|
from app.auth import create_api_key
|
||||||
from app.db import get_connection
|
from app.db import get_connection
|
||||||
@@ -161,7 +161,7 @@ def test_pending_web_global_neschimbat(env):
|
|||||||
|
|
||||||
|
|
||||||
def test_pending_filtreaza_in_sql_cu_regula_null(env):
|
def test_pending_filtreaza_in_sql_cu_regula_null(env):
|
||||||
"""B1: pending_unmapped(conn, account_id=1) include si randuri cu account_id=NULL (legacy).
|
"""pending_unmapped(conn, account_id=1) include si randuri cu account_id=NULL (legacy).
|
||||||
|
|
||||||
Filtrarea trebuie sa se faca IN SQL cu:
|
Filtrarea trebuie sa se faca IN SQL cu:
|
||||||
WHERE status='needs_mapping' AND (account_id=? OR (account_id IS NULL AND ?=1))
|
WHERE status='needs_mapping' AND (account_id=? OR (account_id IS NULL AND ?=1))
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
"""Teste scope cont pe GET /v1/prezentari + /{id} (US-001, PRD 3.2).
|
"""Teste scope cont pe GET /v1/prezentari + /{id}."""
|
||||||
|
|
||||||
Metoda TDD: testele se scriu inainte de implementare (RED) si trebuie sa ramana
|
|
||||||
verzi dupa implementare (GREEN).
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -47,7 +43,7 @@ def test_lista_doar_contul_cheii(env):
|
|||||||
from app.db import get_connection
|
from app.db import get_connection
|
||||||
conn = get_connection()
|
conn = get_connection()
|
||||||
try:
|
try:
|
||||||
# tier='pro' pe ambele conturi — testul verifica scoping GET, nu planuri (T4 PRD 5.17).
|
# tier='pro' pe ambele conturi — testul verifica scoping GET, nu planuri.
|
||||||
conn.execute("UPDATE accounts SET tier='pro' WHERE id=1")
|
conn.execute("UPDATE accounts SET tier='pro' WHERE id=1")
|
||||||
conn.execute("INSERT INTO accounts (id, name, tier) VALUES (2, 'al-doilea', 'pro')")
|
conn.execute("INSERT INTO accounts (id, name, tier) VALUES (2, 'al-doilea', 'pro')")
|
||||||
k1 = create_api_key(conn, 1)
|
k1 = create_api_key(conn, 1)
|
||||||
@@ -77,7 +73,7 @@ def test_lista_doar_contul_cheii(env):
|
|||||||
def test_detaliu_cross_account_404(env):
|
def test_detaliu_cross_account_404(env):
|
||||||
"""GET /{id} cu cheia contului B pentru submission-ul contului A -> 404.
|
"""GET /{id} cu cheia contului B pentru submission-ul contului A -> 404.
|
||||||
|
|
||||||
B3: detail-ul 404 cross-account trebuie byte-identic cu cel al unui id inexistent
|
Raspunsul 404 cross-account trebuie byte-identic cu cel al unui id inexistent
|
||||||
(acelasi status + acelasi mesaj) — nu dam indicii ca randul exista.
|
(acelasi status + acelasi mesaj) — nu dam indicii ca randul exista.
|
||||||
"""
|
"""
|
||||||
with _client() as c:
|
with _client() as c:
|
||||||
@@ -103,7 +99,7 @@ def test_detaliu_cross_account_404(env):
|
|||||||
|
|
||||||
|
|
||||||
def test_legacy_null_vizibil_pentru_cont_1(env):
|
def test_legacy_null_vizibil_pentru_cont_1(env):
|
||||||
"""Randuri cu account_id=NULL apartin contului 1 (legacy OV-2); contul 2 nu le vede."""
|
"""Randuri cu account_id=NULL apartin contului 1 (legacy); contul 2 nu le vede."""
|
||||||
with _client() as c:
|
with _client() as c:
|
||||||
from app.auth import create_api_key
|
from app.auth import create_api_key
|
||||||
from app.db import get_connection
|
from app.db import get_connection
|
||||||
@@ -149,11 +145,11 @@ def test_fara_cheie_flag_off_vede_contul_1(env):
|
|||||||
|
|
||||||
|
|
||||||
def test_detaliu_nu_expune_creds(env):
|
def test_detaliu_nu_expune_creds(env):
|
||||||
"""B4: GET /v1/prezentari/{id} nu expune campuri sensibile (rar_creds_enc, payload_json,
|
"""GET /v1/prezentari/{id} nu expune campuri sensibile (rar_creds_enc, payload_json,
|
||||||
idempotency_key).
|
idempotency_key).
|
||||||
|
|
||||||
NOTA T9 (PRD 5.6): `rar_error` e ACUM expus intentionat (recovery API observabil) —
|
`rar_error` e expus intentionat (recovery API observabil) — contine doar
|
||||||
contine doar coduri/mesaje de validare RAR, niciodata creds.
|
coduri/mesaje de validare RAR, niciodata creds.
|
||||||
"""
|
"""
|
||||||
with _client() as c:
|
with _client() as c:
|
||||||
from app.auth import create_api_key
|
from app.auth import create_api_key
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Teste PRD 5.19 US-010 — onestitate + observabilitate `held` pe canalul API.
|
"""Onestitate + observabilitate `held` pe canalul API.
|
||||||
|
|
||||||
- `held` in raspunsul enqueue (SubmissionResult) + motiv non-null.
|
- `held` in raspunsul enqueue (SubmissionResult) + motiv non-null.
|
||||||
- `held` in proiectiile GET /v1/prezentari si /v1/prezentari/{id}.
|
- `held` in proiectiile GET /v1/prezentari si /v1/prezentari/{id}.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""Teste PRD 5.19 US-002 — snapshot `held` la ingestie din comutatorul de cont.
|
"""Teste snapshot `held` la ingestie din comutatorul de cont.
|
||||||
|
|
||||||
Acopera TOATE situ-rile care scriu `status='queued'` prin ingestie (chokepoint
|
Acopera TOATE situatiile care scriu `status='queued'` prin ingestie (chokepoint
|
||||||
`held_for_account`): enqueue API, reactivare error->queued, dedup echo, commit import,
|
`held_for_account`): enqueue API, reactivare error->queued, dedup echo, commit import,
|
||||||
reresolve `needs_mapping`. `held` NU intra in idempotency_key.
|
reresolve `needs_mapping`. `held` NU intra in idempotency_key.
|
||||||
|
|
||||||
@@ -107,7 +107,7 @@ def test_auto_on_ingestie_not_held(client):
|
|||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------------- #
|
# --------------------------------------------------------------------------- #
|
||||||
# Reactivare error->queued (bug de bypass Eng Finding A) #
|
# Reactivare error->queued #
|
||||||
# --------------------------------------------------------------------------- #
|
# --------------------------------------------------------------------------- #
|
||||||
|
|
||||||
def test_reactivare_respecta_auto_off(client):
|
def test_reactivare_respecta_auto_off(client):
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
"""Regresie PRD 5.19 US-002 — re-snapshot `held` pe caile de re-punere in coada.
|
"""Regresie — re-snapshot `held` pe caile de re-punere in coada.
|
||||||
|
|
||||||
Bug (gasit la /code-review high): create_prezentari (reactivare) si reresolve_account
|
Bug: create_prezentari (reactivare) si reresolve_account re-calculau `held` din
|
||||||
re-calculau `held` din comutatorul contului la tranzitia -> queued, dar caile de
|
comutatorul contului la tranzitia -> queued, dar caile de re-punere din
|
||||||
re-punere din dashboard/admin (requeue_submission, corectie, repune-cu-cod, bulk-fix)
|
dashboard/admin (requeue_submission, corectie, repune-cu-cod, bulk-fix) lasau
|
||||||
lasau `held` pe valoarea VECHE. Consecinta: un rand ingerat pe Auto ON (held=0) care a
|
`held` pe valoarea VECHE. Consecinta: un rand ingerat pe Auto ON (held=0) care a
|
||||||
esuat, apoi contul trecut pe Auto OFF, la re-punere pastra held=0 -> worker-ul il
|
esuat, apoi contul trecut pe Auto OFF, la re-punere pastra held=0 -> worker-ul il
|
||||||
auto-trimitea la RAR (FINALIZATA ireversibil) desi contul e Auto OFF.
|
auto-trimitea la RAR (FINALIZATA ireversibil) desi contul e Auto OFF.
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Teste TDD pentru tools/mapare-llm/heldout_eval.py (L14-S5).
|
"""Teste pentru tools/mapare-llm/heldout_eval.py.
|
||||||
|
|
||||||
Fixture sintetic cu predictii+ground_truth cunoscute. Verifica:
|
Fixture sintetic cu predictii+ground_truth cunoscute. Verifica:
|
||||||
- precizie globala
|
- precizie globala
|
||||||
@@ -398,8 +398,7 @@ class TestExportForLabeling:
|
|||||||
for row in reader:
|
for row in reader:
|
||||||
# Coloana cod_gold trebuie sa fie vida (nu etichetata de cod!)
|
# Coloana cod_gold trebuie sa fie vida (nu etichetata de cod!)
|
||||||
assert row["cod_gold"] == "", (
|
assert row["cod_gold"] == "", (
|
||||||
"cod_gold nu trebuie pre-completat: ar fi 'antrenare pe test' "
|
"cod_gold nu trebuie pre-completat: ar fi 'antrenare pe test'"
|
||||||
"(Decision #19 PRD 5.14)"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_n_randuri_egal_cu_sample(self, tmp_path):
|
def test_n_randuri_egal_cu_sample(self, tmp_path):
|
||||||
@@ -413,7 +412,7 @@ class TestExportForLabeling:
|
|||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Sectiunea 8: kill_criterion — pragul de acceptanta (F-E, PRD 5.14)
|
# Sectiunea 8: kill_criterion — pragul de acceptanta
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
class TestKillCriterion:
|
class TestKillCriterion:
|
||||||
@@ -424,8 +423,8 @@ class TestKillCriterion:
|
|||||||
|
|
||||||
Justificare threshold 0.5% (0.005):
|
Justificare threshold 0.5% (0.005):
|
||||||
Un service cu 200 operatii/zi auto-rezolvate = 1 FINALIZATA gresita/zi.
|
Un service cu 200 operatii/zi auto-rezolvate = 1 FINALIZATA gresita/zi.
|
||||||
FINALIZATA e ireversibila (cf. PRD 5.14 Premisa 3 / invariant CLAUDE.md).
|
FINALIZATA e ireversibila.
|
||||||
Pragul poate fi RELAXAT empiric; nu INASPRIT post-hoc (sesizare-in-timp).
|
Pragul poate fi RELAXAT empiric; nu INASPRIT post-hoc.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def test_trece_cand_sub_prag(self):
|
def test_trece_cand_sub_prag(self):
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""US-003 (PRD 5.20): build_key incorporeaza rar_env."""
|
"""build_key incorporeaza rar_env."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
"""Teste API import Treapta 2 — POST /v1/import, preview, commit, export-failed.
|
"""Teste API import — POST /v1/import, preview, commit, export-failed.
|
||||||
|
|
||||||
Acopera:
|
Acopera:
|
||||||
- #11 U1+T4: upload + staging + mapare coloane semnatura/drift/fuzzy
|
- upload + staging + mapare coloane semnatura/drift/fuzzy
|
||||||
- #12 T2+T11: preview 6 stari + already_sent batch lookup + intra-batch collision
|
- preview 6 stari + already_sent batch lookup + intra-batch collision
|
||||||
- #13 T5+T12: gate HARD confirmare + atestare valori + commit ON CONFLICT (TOCTOU)
|
- gate HARD confirmare + atestare valori + commit ON CONFLICT (TOCTOU)
|
||||||
- #14 T8: export randuri esuate CSV
|
- export randuri esuate CSV
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -91,15 +91,11 @@ def _default_column_mapping() -> dict:
|
|||||||
|
|
||||||
def _setup_nomenclator(client: TestClient) -> None:
|
def _setup_nomenclator(client: TestClient) -> None:
|
||||||
"""Seed nomenclator cu un cod de prestatie pentru teste."""
|
"""Seed nomenclator cu un cod de prestatie pentru teste."""
|
||||||
# Folosim POST /v1/prezentari pentru a forta seed-ul nomenclatorului
|
|
||||||
# care are loc in init_db -> seed_nomenclator_if_empty
|
|
||||||
pass # seed-ul se face automat in init_db
|
pass # seed-ul se face automat in init_db
|
||||||
|
|
||||||
|
|
||||||
def _seed_operation_mapping(client: TestClient, cod_op: str = "Revizie", cod_prest: str = "OE-1") -> None:
|
def _seed_operation_mapping(client: TestClient, cod_op: str = "Revizie", cod_prest: str = "OE-1") -> None:
|
||||||
"""Salveaza o mapare de operatii pentru teste."""
|
"""Salveaza o mapare de operatii pentru teste."""
|
||||||
# Adauga mai intai in nomenclator daca nu exista (prin POST prezentare care creeaza cod)
|
|
||||||
# De fapt, cod OE-1 e in nomenclatorul seed
|
|
||||||
client.post("/v1/mapari", json={
|
client.post("/v1/mapari", json={
|
||||||
"cod_op_service": cod_op,
|
"cod_op_service": cod_op,
|
||||||
"cod_prestatie": cod_prest,
|
"cod_prestatie": cod_prest,
|
||||||
@@ -108,7 +104,7 @@ def _seed_operation_mapping(client: TestClient, cod_op: str = "Revizie", cod_pre
|
|||||||
|
|
||||||
|
|
||||||
# =========================================================================== #
|
# =========================================================================== #
|
||||||
# #11 — Upload + staging (U1+T4) #
|
# Upload + staging #
|
||||||
# =========================================================================== #
|
# =========================================================================== #
|
||||||
|
|
||||||
class TestUploadStaging:
|
class TestUploadStaging:
|
||||||
@@ -144,7 +140,7 @@ class TestUploadStaging:
|
|||||||
assert r.status_code == 422
|
assert r.status_code == 422
|
||||||
|
|
||||||
def test_issue5a_raw_json_criptat(self, client):
|
def test_issue5a_raw_json_criptat(self, client):
|
||||||
"""Issue 5a: raw_json din import_rows trebuie sa fie criptat (ciphertext la rest)."""
|
"""raw_json din import_rows trebuie sa fie criptat (ciphertext la rest)."""
|
||||||
data = _make_xlsx([_HEADER, _ROW_OK])
|
data = _make_xlsx([_HEADER, _ROW_OK])
|
||||||
r = _upload_file(client, data, "test.xlsx")
|
r = _upload_file(client, data, "test.xlsx")
|
||||||
assert r.status_code == 200
|
assert r.status_code == 200
|
||||||
@@ -173,7 +169,7 @@ class TestUploadStaging:
|
|||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
def test_issue5b_fuzzy_coloane_refoloseste_normalize_for_match(self, client):
|
def test_issue5b_fuzzy_coloane_refoloseste_normalize_for_match(self, client):
|
||||||
"""Issue 5b: fuzzy_suggestions din raspuns foloseste normalize_for_match (fara duplicat)."""
|
"""fuzzy_suggestions din raspuns foloseste normalize_for_match (fara duplicat)."""
|
||||||
data = _make_xlsx([_HEADER, _ROW_OK])
|
data = _make_xlsx([_HEADER, _ROW_OK])
|
||||||
r = _upload_file(client, data, "test.xlsx")
|
r = _upload_file(client, data, "test.xlsx")
|
||||||
assert r.status_code == 200
|
assert r.status_code == 200
|
||||||
@@ -190,7 +186,7 @@ class TestUploadStaging:
|
|||||||
assert "odometru_final" in camps, f"'odometru_final' trebuie in sugestii"
|
assert "odometru_final" in camps, f"'odometru_final' trebuie in sugestii"
|
||||||
|
|
||||||
def test_drift_semnatura_coloane(self, client):
|
def test_drift_semnatura_coloane(self, client):
|
||||||
"""T4/D3: upload 2 cu coloane mutate -> mapping_status='new' (nu aplica orb)."""
|
"""Upload 2 cu coloane mutate -> mapping_status='new' (nu aplica orb)."""
|
||||||
# Upload 1 cu header standard
|
# Upload 1 cu header standard
|
||||||
data1 = _make_xlsx([_HEADER, _ROW_OK])
|
data1 = _make_xlsx([_HEADER, _ROW_OK])
|
||||||
r1 = _upload_file(client, data1, "test.xlsx")
|
r1 = _upload_file(client, data1, "test.xlsx")
|
||||||
@@ -275,7 +271,7 @@ class TestUploadStaging:
|
|||||||
assert body["sample_rows"][0]["VIN"] == _ROW_OK2[0]
|
assert body["sample_rows"][0]["VIN"] == _ROW_OK2[0]
|
||||||
|
|
||||||
def test_purge_after_setat_la_insert(self, client):
|
def test_purge_after_setat_la_insert(self, client):
|
||||||
"""T16: purge_after trebuie setat la insert import_batches."""
|
"""purge_after trebuie setat la insert import_batches."""
|
||||||
data = _make_xlsx([_HEADER, _ROW_OK])
|
data = _make_xlsx([_HEADER, _ROW_OK])
|
||||||
r = _upload_file(client, data, "test.xlsx")
|
r = _upload_file(client, data, "test.xlsx")
|
||||||
import_id = r.json()["import_id"]
|
import_id = r.json()["import_id"]
|
||||||
@@ -294,7 +290,7 @@ class TestUploadStaging:
|
|||||||
|
|
||||||
|
|
||||||
# =========================================================================== #
|
# =========================================================================== #
|
||||||
# #11 — Mapare coloane (T4) #
|
# Mapare coloane #
|
||||||
# =========================================================================== #
|
# =========================================================================== #
|
||||||
|
|
||||||
class TestColumnMapping:
|
class TestColumnMapping:
|
||||||
@@ -349,7 +345,7 @@ class TestColumnMapping:
|
|||||||
|
|
||||||
|
|
||||||
# =========================================================================== #
|
# =========================================================================== #
|
||||||
# #12 — Preview 6 stari (T2 + T11) #
|
# Preview 6 stari #
|
||||||
# =========================================================================== #
|
# =========================================================================== #
|
||||||
|
|
||||||
class TestPreview:
|
class TestPreview:
|
||||||
@@ -405,7 +401,7 @@ class TestPreview:
|
|||||||
assert any(r["resolved_status"] in ("needs_data",) for r in body["rows"])
|
assert any(r["resolved_status"] in ("needs_data",) for r in body["rows"])
|
||||||
|
|
||||||
def test_preview_already_sent_dupa_submit(self, client):
|
def test_preview_already_sent_dupa_submit(self, client):
|
||||||
"""Rand deja trimis prin API -> stare already_sent la preview (T2/D5)."""
|
"""Rand deja trimis prin API -> stare already_sent la preview."""
|
||||||
# Trimite prin API canalul standard
|
# Trimite prin API canalul standard
|
||||||
client.post("/v1/prezentari", json={
|
client.post("/v1/prezentari", json={
|
||||||
"rar_credentials": {"email": "x@y.ro", "password": "s"},
|
"rar_credentials": {"email": "x@y.ro", "password": "s"},
|
||||||
@@ -430,7 +426,7 @@ class TestPreview:
|
|||||||
assert "already_sent" in statuses, f"Asteptat 'already_sent', primit: {statuses}"
|
assert "already_sent" in statuses, f"Asteptat 'already_sent', primit: {statuses}"
|
||||||
|
|
||||||
def test_preview_duplicate_in_file(self, client):
|
def test_preview_duplicate_in_file(self, client):
|
||||||
"""T11/OV-3: 2 randuri identice in ACELASI fisier -> duplicate_in_file."""
|
"""2 randuri identice in ACELASI fisier -> duplicate_in_file."""
|
||||||
# Acelasi rand de doua ori
|
# Acelasi rand de doua ori
|
||||||
rows = [_HEADER, _ROW_OK, _ROW_OK] # duplicat exact
|
rows = [_HEADER, _ROW_OK, _ROW_OK] # duplicat exact
|
||||||
import_id = self._upload_and_map(client, rows=rows)
|
import_id = self._upload_and_map(client, rows=rows)
|
||||||
@@ -444,7 +440,7 @@ class TestPreview:
|
|||||||
f"Asteptat 'duplicate_in_file', primit: {statuses}"
|
f"Asteptat 'duplicate_in_file', primit: {statuses}"
|
||||||
|
|
||||||
def test_preview_already_sent_batch_lookup_nu_n_plus_1(self, client):
|
def test_preview_already_sent_batch_lookup_nu_n_plus_1(self, client):
|
||||||
"""Eng#5: already_sent lookup BATCH (nu N+1) — ≤7 interogari pentru 5 randuri."""
|
"""already_sent lookup BATCH (nu N+1) — ≤7 interogari pentru 5 randuri."""
|
||||||
# Cream 5 randuri distincte
|
# Cream 5 randuri distincte
|
||||||
rows_data = [_HEADER]
|
rows_data = [_HEADER]
|
||||||
for i in range(5):
|
for i in range(5):
|
||||||
@@ -492,7 +488,7 @@ class TestPreview:
|
|||||||
|
|
||||||
|
|
||||||
# =========================================================================== #
|
# =========================================================================== #
|
||||||
# #13 — Commit gate HARD + atestare + TOCTOU (T5 + T12) #
|
# Commit gate HARD + atestare + TOCTOU #
|
||||||
# =========================================================================== #
|
# =========================================================================== #
|
||||||
|
|
||||||
class TestCommit:
|
class TestCommit:
|
||||||
@@ -528,7 +524,7 @@ class TestCommit:
|
|||||||
assert body["enqueued"] == n_ok
|
assert body["enqueued"] == n_ok
|
||||||
|
|
||||||
def test_commit_cu_n_gresit_reject(self, client):
|
def test_commit_cu_n_gresit_reject(self, client):
|
||||||
"""T5/D3: commit cu N gresit -> 422, nu enqueue."""
|
"""Commit cu N gresit -> 422, nu enqueue."""
|
||||||
import_id, preview = self._upload_preview_ok(client)
|
import_id, preview = self._upload_preview_ok(client)
|
||||||
n_ok = preview["summary"].get("ok", 0)
|
n_ok = preview["summary"].get("ok", 0)
|
||||||
|
|
||||||
@@ -543,7 +539,7 @@ class TestCommit:
|
|||||||
f"Eroare neasteptata: {detail}"
|
f"Eroare neasteptata: {detail}"
|
||||||
|
|
||||||
def test_commit_log_atestare(self, client):
|
def test_commit_log_atestare(self, client):
|
||||||
"""T12/Voce#9: commit scrie import_attestations cu rows_hash + n_confirmed."""
|
"""Commit scrie import_attestations cu rows_hash + n_confirmed."""
|
||||||
import_id, preview = self._upload_preview_ok(client)
|
import_id, preview = self._upload_preview_ok(client)
|
||||||
n_ok = preview["summary"].get("ok", 0)
|
n_ok = preview["summary"].get("ok", 0)
|
||||||
|
|
||||||
@@ -574,7 +570,7 @@ class TestCommit:
|
|||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
def test_commit_batch_id_setat_pe_submission(self, client):
|
def test_commit_batch_id_setat_pe_submission(self, client):
|
||||||
"""T7: submission creata la commit trebuie sa aiba batch_id + row_index setate."""
|
"""Submission creata la commit trebuie sa aiba batch_id + row_index setate."""
|
||||||
import_id, preview = self._upload_preview_ok(client)
|
import_id, preview = self._upload_preview_ok(client)
|
||||||
n_ok = preview["summary"].get("ok", 0)
|
n_ok = preview["summary"].get("ok", 0)
|
||||||
|
|
||||||
@@ -602,7 +598,7 @@ class TestCommit:
|
|||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
def test_commit_toctou_cheie_inserata_concurent(self, client):
|
def test_commit_toctou_cheie_inserata_concurent(self, client):
|
||||||
"""Issue 1 (TOCTOU): cheie inserata de canal concurent -> reclasificata already_sent.
|
"""Cheie inserata de canal concurent (TOCTOU) -> reclasificata already_sent.
|
||||||
|
|
||||||
Simulam TOCTOU inserand cheia direct in submissions inainte de commit.
|
Simulam TOCTOU inserand cheia direct in submissions inainte de commit.
|
||||||
"""
|
"""
|
||||||
@@ -635,7 +631,6 @@ class TestCommit:
|
|||||||
"reviewed_rows": [],
|
"reviewed_rows": [],
|
||||||
})
|
})
|
||||||
# Poate fi 200 cu toctou_collisions sau 422 cu informatii clare
|
# Poate fi 200 cu toctou_collisions sau 422 cu informatii clare
|
||||||
# Conform planului Issue 1: reclasificat already_sent, nu rollback
|
|
||||||
# Dar n_enqueued va fi 0 daca toate colideaza
|
# Dar n_enqueued va fi 0 daca toate colideaza
|
||||||
if rc.status_code == 200:
|
if rc.status_code == 200:
|
||||||
body = rc.json()
|
body = rc.json()
|
||||||
@@ -644,7 +639,7 @@ class TestCommit:
|
|||||||
# (dupa reclasificare, n_total_ok scade)
|
# (dupa reclasificare, n_total_ok scade)
|
||||||
|
|
||||||
def test_commit_needs_review_nebifat_exclus_din_n(self, client):
|
def test_commit_needs_review_nebifat_exclus_din_n(self, client):
|
||||||
"""Voce#1: rand needs_review nebifat explicit -> NU intra in N, NU se enqueued."""
|
"""Rand needs_review nebifat explicit -> NU intra in N, NU se enqueued."""
|
||||||
# Rand cu VIN numeric (coercion -> needs_review)
|
# Rand cu VIN numeric (coercion -> needs_review)
|
||||||
import datetime as dt
|
import datetime as dt
|
||||||
wb = openpyxl.Workbook()
|
wb = openpyxl.Workbook()
|
||||||
@@ -704,7 +699,7 @@ class TestCommit:
|
|||||||
assert r2.status_code == 409
|
assert r2.status_code == 409
|
||||||
|
|
||||||
def test_atestare_purge_after_setat_pe_submission(self, client):
|
def test_atestare_purge_after_setat_pe_submission(self, client):
|
||||||
"""T16: submissions create la commit trebuie sa aiba purge_after setat."""
|
"""Submissions create la commit trebuie sa aiba purge_after setat."""
|
||||||
import_id, preview = self._upload_preview_ok(client)
|
import_id, preview = self._upload_preview_ok(client)
|
||||||
n_ok = preview["summary"].get("ok", 0)
|
n_ok = preview["summary"].get("ok", 0)
|
||||||
|
|
||||||
@@ -732,7 +727,7 @@ class TestCommit:
|
|||||||
|
|
||||||
|
|
||||||
# =========================================================================== #
|
# =========================================================================== #
|
||||||
# #14 — Export randuri esuate CSV (T8) #
|
# Export randuri esuate CSV #
|
||||||
# =========================================================================== #
|
# =========================================================================== #
|
||||||
|
|
||||||
class TestExportFailed:
|
class TestExportFailed:
|
||||||
@@ -806,12 +801,12 @@ class TestExportFailed:
|
|||||||
|
|
||||||
|
|
||||||
# =========================================================================== #
|
# =========================================================================== #
|
||||||
# Regresie: reconcile.py ramane op-blind (OV-3) #
|
# Regresie: reconcile.py ramane op-blind #
|
||||||
# =========================================================================== #
|
# =========================================================================== #
|
||||||
|
|
||||||
class TestReconcileRegresie:
|
class TestReconcileRegresie:
|
||||||
def test_match_finalizata_ramane_op_blind(self):
|
def test_match_finalizata_ramane_op_blind(self):
|
||||||
"""OV-3: reconcile.py trebuie sa ramana op-blind (nu editat de import).
|
"""reconcile.py trebuie sa ramana op-blind (nu editat de import).
|
||||||
|
|
||||||
Importam reconcile si verificam ca nu s-au adaugat parametri de operatie.
|
Importam reconcile si verificam ca nu s-au adaugat parametri de operatie.
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
"""Teste US-007 (PRD 5.11) — Post-commit: lista Trimiteri apare + refresh auto.
|
"""Teste post-commit: lista Trimiteri apare + refresh auto.
|
||||||
|
|
||||||
TDD RED: testele sunt scrise inainte de implementare.
|
|
||||||
Verifica:
|
Verifica:
|
||||||
1. Raspunsul confirma emite header HX-Trigger: trimiteriChanged.
|
1. Raspunsul confirma emite header HX-Trigger: trimiteriChanged.
|
||||||
2. Raspunsul confirma include OOB swap al #trimiteri-section cu submissions-wrap.
|
2. Raspunsul confirma include OOB swap al #trimiteri-section cu submissions-wrap.
|
||||||
@@ -130,7 +129,7 @@ _ROWS_OK = [
|
|||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------------- #
|
# --------------------------------------------------------------------------- #
|
||||||
# Teste RED → GREEN #
|
# Teste #
|
||||||
# --------------------------------------------------------------------------- #
|
# --------------------------------------------------------------------------- #
|
||||||
|
|
||||||
def test_confirma_emite_hx_trigger(client):
|
def test_confirma_emite_hx_trigger(client):
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
"""Teste E2E T15 — integrare import->commit->worker (RAR mock) + masina de stari + failure registry.
|
"""Teste E2E: integrare import->commit->worker (RAR mock) + masina de stari + failure registry.
|
||||||
|
|
||||||
Acopera (plan T15, sect.12):
|
Acopera:
|
||||||
- Scenariul 1: upload xlsx/csv -> column-mapping -> preview -> commit N
|
- Scenariul 1: upload xlsx/csv -> column-mapping -> preview -> commit N
|
||||||
-> worker process_one (MockRar) -> submission FINALIZATA cu id_prezentare.
|
-> worker process_one (MockRar) -> submission FINALIZATA cu id_prezentare.
|
||||||
- Scenariul 2: re-upload acelasi continut -> preview marcheaza already_sent
|
- Scenariul 2: re-upload acelasi continut -> preview marcheaza already_sent
|
||||||
(NU al doilea FINALIZATA dupa commit).
|
(NU al doilea FINALIZATA dupa commit).
|
||||||
- Scenariul 3: coada MIXTA API(creds efemere)+web(creds durabile per-env, US-013):
|
- Scenariul 3: coada mixta API(creds efemere)+web(creds durabile per-env):
|
||||||
dupa login + purjare creds efemere, submission-urile web tot se trimit
|
dupa login + purjare creds efemere, submission-urile web tot se trimit
|
||||||
prin fallback accounts.rar_creds_{env}_enc (T1/Voce#5).
|
prin fallback accounts.rar_creds_{env}_enc.
|
||||||
- Masina de stari (sect. 6): tranzitii queued->sending->sent/requeued/error.
|
- Masina de stari: tranzitii queued->sending->sent/requeued/error.
|
||||||
- Failure registry (sect. 8): 400/403/503+reconciliere.
|
- Failure registry: 400/403/503+reconciliere.
|
||||||
- T16: purge_expired + purge_after setat la commit.
|
- Purjare: purge_expired + purge_after setat la commit.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -451,10 +451,10 @@ class TestE2EAlreadySent:
|
|||||||
|
|
||||||
class TestE2EMixedQueue:
|
class TestE2EMixedQueue:
|
||||||
def test_coada_mixta_ambele_trimise(self, env, monkeypatch):
|
def test_coada_mixta_ambele_trimise(self, env, monkeypatch):
|
||||||
"""Scenariul 3 (T1/Voce#5): API (creds efemere) + web (canal import, fara creds pe submission).
|
"""Scenariul 3: API (creds efemere) + web (canal import, fara creds pe submission).
|
||||||
|
|
||||||
Dupa purjarea creds efemere ale canalului API (la primul login), submission-urile
|
Dupa purjarea creds efemere ale canalului API (la primul login), submission-urile
|
||||||
import (web) tot se trimit prin fallback accounts.rar_creds_{env}_enc (US-013, per-env).
|
import (web) tot se trimit prin fallback accounts.rar_creds_{env}_enc, per-env.
|
||||||
Verificam ca ambele submission-uri ajung la status='sent'.
|
Verificam ca ambele submission-uri ajung la status='sent'.
|
||||||
"""
|
"""
|
||||||
import app.worker.__main__ as w
|
import app.worker.__main__ as w
|
||||||
@@ -486,11 +486,11 @@ class TestE2EMixedQueue:
|
|||||||
assert r_api.status_code == 200, r_api.text
|
assert r_api.status_code == 200, r_api.text
|
||||||
sub_api_id = r_api.json()["results"][0]["submission_id"]
|
sub_api_id = r_api.json()["results"][0]["submission_id"]
|
||||||
|
|
||||||
# 3. Seteaza creds durabile in accounts per-env (US-013, canal web fallback)
|
# 3. Seteaza creds durabile in accounts per-env (canal web fallback)
|
||||||
conn = get_connection()
|
conn = get_connection()
|
||||||
try:
|
try:
|
||||||
web_creds_enc = encrypt_creds({"email": "web@test.ro", "password": "pass_web"})
|
web_creds_enc = encrypt_creds({"email": "web@test.ro", "password": "pass_web"})
|
||||||
# US-013: scrie in slotul per-env test (rar_env default = 'test')
|
# scrie in slotul per-env test (rar_env default = 'test')
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"UPDATE accounts SET rar_creds_test_enc=?, rar_test_enabled=1 WHERE id=1",
|
"UPDATE accounts SET rar_creds_test_enc=?, rar_test_enabled=1 WHERE id=1",
|
||||||
(web_creds_enc,),
|
(web_creds_enc,),
|
||||||
@@ -522,7 +522,7 @@ class TestE2EMixedQueue:
|
|||||||
try:
|
try:
|
||||||
processed = 0
|
processed = 0
|
||||||
|
|
||||||
# Drena coada simuland bucla worker completa (T1/D4: creds efemere + fallback per-env)
|
# Drena coada simuland bucla worker completa: creds efemere + fallback per-env
|
||||||
for _ in range(10): # limita de siguranta
|
for _ in range(10): # limita de siguranta
|
||||||
claimed = w.claim_one(conn)
|
claimed = w.claim_one(conn)
|
||||||
if claimed is None:
|
if claimed is None:
|
||||||
@@ -532,7 +532,7 @@ class TestE2EMixedQueue:
|
|||||||
account_id = claimed["account_id"]
|
account_id = claimed["account_id"]
|
||||||
rar_env = claimed.get("rar_env", "test")
|
rar_env = claimed.get("rar_env", "test")
|
||||||
|
|
||||||
# T1/D4: creds din submission (API efemer) OR fallback per-env (US-013)
|
# creds din submission (API efemer) OR fallback per-env
|
||||||
creds = w._creds_for(claimed, settings) or w._creds_from_account(conn, account_id, rar_env=rar_env)
|
creds = w._creds_for(claimed, settings) or w._creds_from_account(conn, account_id, rar_env=rar_env)
|
||||||
assert creds is not None, \
|
assert creds is not None, \
|
||||||
f"Creds None pentru submission {sid} — fallback durabil trebuie sa existe"
|
f"Creds None pentru submission {sid} — fallback durabil trebuie sa existe"
|
||||||
@@ -566,9 +566,9 @@ class TestE2EMixedQueue:
|
|||||||
assert mock_rar.post_calls == 2
|
assert mock_rar.post_calls == 2
|
||||||
|
|
||||||
def test_purjare_creds_efemere_nu_sterge_durabile(self, env, monkeypatch):
|
def test_purjare_creds_efemere_nu_sterge_durabile(self, env, monkeypatch):
|
||||||
"""T1/Gate purjare (OV-5): dupa login, submissions.rar_creds_enc sterse DAR per-env INTACT.
|
"""Dupa login, submissions.rar_creds_enc sterse DAR per-env INTACT.
|
||||||
|
|
||||||
Worker sterge DOAR submissions.rar_creds_enc (efemere), NU accounts.rar_creds_{env}_enc (durabile, US-013).
|
Worker sterge DOAR submissions.rar_creds_enc (efemere), NU accounts.rar_creds_{env}_enc (durabile).
|
||||||
"""
|
"""
|
||||||
import app.worker.__main__ as w
|
import app.worker.__main__ as w
|
||||||
from app.crypto import encrypt_creds
|
from app.crypto import encrypt_creds
|
||||||
@@ -590,7 +590,7 @@ class TestE2EMixedQueue:
|
|||||||
assert r_api.status_code == 200
|
assert r_api.status_code == 200
|
||||||
sub_id = r_api.json()["results"][0]["submission_id"]
|
sub_id = r_api.json()["results"][0]["submission_id"]
|
||||||
|
|
||||||
# Seteaza creds durabile pe cont (slotul per-env, US-013)
|
# Seteaza creds durabile pe cont (slotul per-env)
|
||||||
conn = get_connection()
|
conn = get_connection()
|
||||||
try:
|
try:
|
||||||
web_creds_enc = encrypt_creds({"email": "web@test.ro", "password": "pass_web"})
|
web_creds_enc = encrypt_creds({"email": "web@test.ro", "password": "pass_web"})
|
||||||
@@ -634,7 +634,7 @@ class TestE2EMixedQueue:
|
|||||||
|
|
||||||
|
|
||||||
# =========================================================================== #
|
# =========================================================================== #
|
||||||
# Masina de stari (sect. 6 din plan): tranzitii explicite per submission #
|
# Masina de stari: tranzitii explicite per submission #
|
||||||
# =========================================================================== #
|
# =========================================================================== #
|
||||||
|
|
||||||
class TestStateMachine:
|
class TestStateMachine:
|
||||||
@@ -797,7 +797,7 @@ class TestStateMachine:
|
|||||||
|
|
||||||
|
|
||||||
# =========================================================================== #
|
# =========================================================================== #
|
||||||
# Registrul de esecuri (sect. 8): tipuri de eroare RAR si recuperare #
|
# Registrul de esecuri: tipuri de eroare RAR si recuperare #
|
||||||
# =========================================================================== #
|
# =========================================================================== #
|
||||||
|
|
||||||
class TestFailureRegistry:
|
class TestFailureRegistry:
|
||||||
@@ -929,12 +929,12 @@ class TestFailureRegistry:
|
|||||||
|
|
||||||
|
|
||||||
# =========================================================================== #
|
# =========================================================================== #
|
||||||
# T16 — Purjare (purge_expired + purge_after setat la commit) #
|
# Purjare (purge_expired + purge_after setat la commit) #
|
||||||
# =========================================================================== #
|
# =========================================================================== #
|
||||||
|
|
||||||
class TestPurjare:
|
class TestPurjare:
|
||||||
def test_purge_expired_sterge_sent_expirate(self, env):
|
def test_purge_expired_sterge_sent_expirate(self, env):
|
||||||
"""T16: purge_expired sterge submissions cu status='sent' si purge_after in trecut."""
|
"""purge_expired sterge submissions cu status='sent' si purge_after in trecut."""
|
||||||
from app.worker.__main__ import purge_expired
|
from app.worker.__main__ import purge_expired
|
||||||
|
|
||||||
client, get_connection, get_settings = env
|
client, get_connection, get_settings = env
|
||||||
@@ -958,7 +958,7 @@ class TestPurjare:
|
|||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
def test_purge_expired_nu_sterge_queued(self, env):
|
def test_purge_expired_nu_sterge_queued(self, env):
|
||||||
"""T16: purge_expired NU sterge non-sent (queued/error) chiar daca purge_after < now."""
|
"""purge_expired NU sterge non-sent (queued/error) chiar daca purge_after < now."""
|
||||||
from app.worker.__main__ import purge_expired
|
from app.worker.__main__ import purge_expired
|
||||||
|
|
||||||
client, get_connection, get_settings = env
|
client, get_connection, get_settings = env
|
||||||
@@ -981,7 +981,7 @@ class TestPurjare:
|
|||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
def test_purge_after_setat_la_commit_import(self, env):
|
def test_purge_after_setat_la_commit_import(self, env):
|
||||||
"""T16: submissions create la commit import au purge_after setat (!=NULL)."""
|
"""submissions create la commit import au purge_after setat (!=NULL)."""
|
||||||
client, get_connection, get_settings = env
|
client, get_connection, get_settings = env
|
||||||
|
|
||||||
import_id = _upload_and_map(client, rows=[_HEADER, _ROW_1])
|
import_id = _upload_and_map(client, rows=[_HEADER, _ROW_1])
|
||||||
@@ -1005,7 +1005,7 @@ class TestPurjare:
|
|||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
def test_purge_after_setat_la_trimitere_worker(self, env):
|
def test_purge_after_setat_la_trimitere_worker(self, env):
|
||||||
"""T16: dupa trimitere (status='sent'), purge_after se (re)seteaza la now+90 zile."""
|
"""dupa trimitere (status='sent'), purge_after se (re)seteaza la now+90 zile."""
|
||||||
import app.worker.__main__ as w
|
import app.worker.__main__ as w
|
||||||
|
|
||||||
client, get_connection, get_settings = env
|
client, get_connection, get_settings = env
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
"""Teste US-001 (PRD 3.6): backend — persista editarea unui rand de preview.
|
"""Teste backend — persista editarea unui rand de preview.
|
||||||
|
|
||||||
Approach B: editarea scrie un patch CANONIC in import_rows.override_json (criptat
|
Editarea scrie un patch CANONIC in import_rows.override_json (criptat
|
||||||
Fernet), aplicat ULTIMUL in _resolve_row_for_preview + commit. raw_json/idempotency
|
Fernet), aplicat ULTIMUL in _resolve_row_for_preview + commit. raw_json/idempotency
|
||||||
raman neatinse. Ruta editare = mutatie PURA (nu re-deriva status, nu atinge submissions).
|
raman neatinse. Ruta editare = mutatie PURA (nu re-deriva status, nu atinge submissions).
|
||||||
|
|
||||||
TDD: testele se scriu inainte de implementare (RED -> GREEN).
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user