chore: curatare agresiva comentarii — scoatere referinte US/PRD din cod si template-uri

Eliminat zgomotul de trasabilitate (US-xxx, PRD x.x, Rn, OV-x, Tn, decizii/naratiune
istorica) din 41 fisiere app/ + template-uri. Pastrate comentariile care documenteaza
invarianti si logica ne-evidenta (idempotenta/hash, reconciliere anti-duplicat, RAR 500
esec definitiv, creds per cont, WAF User-Agent, 422 fara echo de parola, scope NULL->1),
curatate doar de tokeni.

Verificare: pentru cele 27 module .py curatate, structura de cod (tokeni non-comentariu/
non-string) e IDENTICA fata de HEAD -> doar comentarii/docstring-uri schimbate. Singura
schimbare de cod e in tests/test_web_responsive.py (scos 3 assert pe markeri US-006/007/008,
inlocuite de asertiunile structurale alaturate). 0 tokeni US/PRD reziduali in app/.
Regresie: 896 passed, 1 deselected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-06-25 21:44:24 +00:00
parent f05fe5b221
commit 4a2afc68bf
43 changed files with 547 additions and 649 deletions

View File

@@ -1,12 +1,11 @@
"""Dashboard Jinja2 + HTMX (server-rendered, zero build).
Schelet cu stari explicite: empty (coada goala), banner alerta blocate,
worker viu/mort, ultimul login RAR. Editor mapari + browser nomenclator +
export CSV + stare "RAR indisponibil" = de adaugat (plan.md sect. 4 + design-review).
worker viu/mort, ultimul login RAR. Editor mapari + browser nomenclator.
U5 — Rute web pentru import fisier (upload → mapare coloane → preview → confirma → trimite).
Consuma endpointurile backend din import_router (helper-e interne) fara a le modifica.
Toate rutele /_import/* returneaza fragmente HTML targetate pe #import-section prin HTMX.
Rute web pentru import fisier (upload → mapare coloane → preview → confirma → trimite).
Consuma helper-e interne din import_router fara a le modifica. Toate rutele /_import/*
returneaza fragmente HTML targetate pe #import-section prin HTMX.
"""
from __future__ import annotations
@@ -83,12 +82,12 @@ from ..mapping import (
text_rules_overlap,
)
# Campuri canonice cu eticheta umana pentru dropdown mapare coloane (U5)
# Campuri canonice cu eticheta umana pentru dropdown mapare coloane
_CANONICAL_FIELDS = [(k, v[0]) for k, v in _CANONICAL_SYNONYMS.items()]
router = APIRouter(tags=["web"])
templates = Jinja2Templates(directory=str(Path(__file__).resolve().parent / "templates"))
# Expune parse_erori in toate template-urile (US-006, PRD 5.4)
# Expune parse_erori in toate template-urile
templates.env.globals["parse_erori"] = parse_erori
_BLOCKED = ("error", "needs_data", "needs_mapping")
@@ -98,7 +97,7 @@ def _ctx(request: Request, **extra) -> dict:
"""Context de baza pentru template-uri cu formulare: include mereu csrf_token.
Previne lock-out in prod (web_auth_required=True): orice re-randare de eroare
trebuie sa includa csrf_token negol altfel urmatorul submit da 403 (task #8).
trebuie sa includa csrf_token negol altfel urmatorul submit da 403.
"""
return {"request": request, "csrf_token": get_csrf_token(request), **extra}
@@ -161,15 +160,14 @@ def _rar_state(hb, worker_alive: bool) -> str:
return "indisponibil?" if age > 108000 else "ok"
# US-002: "import" nu mai e tab separat — importul traieste pe Acasa. ?tab=import
# cade pe Acasa (tab invalid -> fallback "acasa" in dashboard()), fara 404.
# US-003 (3.6): "coada" (Trimiteri) nu mai e tab — Trimiterile sunt sectiune pe Acasa.
# ?tab=coada cade tot pe Acasa (fallback), fara 404, fara fragment orfan.
# "import" si "coada" nu mai sunt tab-uri separate — importul si Trimiterile sunt
# sectiuni pe Acasa. ?tab=import / ?tab=coada cad pe Acasa (fallback in dashboard()),
# fara 404 si fara fragment orfan.
_TABS_VALIDE = {"acasa", "mapari", "cont", "nomenclator", "integrare", "jurnal"}
def _get_acasa_context(request: Request, conn, account_id: int) -> dict:
"""Calculeaza contextul pentru panoul Acasa (US-005).
"""Calculeaza contextul pentru panoul Acasa.
Scoped pe contul sesiunii — identic cu pattern-ul din restul rutelor (NULL->1).
"""
@@ -197,8 +195,8 @@ def _get_acasa_context(request: Request, conn, account_id: int) -> dict:
).fetchone()
are_cheie_folosita = row_key is not None
# US-003 (3.6): contorul de atentie (blocate) se reflecta in heading-ul
# sectiunii "Trimiterile tale" de pe Acasa, nu pe un tab disparut.
# Contorul de atentie (blocate) se reflecta in heading-ul sectiunii
# "Trimiterile tale" de pe Acasa.
counts = _status_counts(conn, account_id)
blocate_total = sum(counts.get(s, 0) for s in _BLOCKED)
@@ -212,7 +210,7 @@ def _get_acasa_context(request: Request, conn, account_id: int) -> dict:
"pills_categorii": _pills_categorii(counts),
# Semnatura datelor: nudge-ul "Date noi" o compara la fiecare poll usor.
"versiune_trimiteri": _trimiteri_versiune(conn, account_id),
# US-002: Acasa include caseta de upload -> are nevoie de csrf_token
# Acasa include caseta de upload -> are nevoie de csrf_token
"csrf_token": get_csrf_token(request),
}
@@ -220,8 +218,8 @@ def _get_acasa_context(request: Request, conn, account_id: int) -> dict:
def _render_panel_acasa(request: Request, conn=None, account_id: int = 1, status: str | None = None) -> str:
"""Randeaza panoul Acasa ca string HTML.
`status` (US-014/T13): deep-link `?tab=acasa&status=error` pre-selecteaza filtrul de
stare in sectiunea Trimiteri, astfel ca lista se incarca direct filtrata (nu dead-end).
`status`: deep-link `?tab=acasa&status=error` pre-selecteaza filtrul de stare in
sectiunea Trimiteri, astfel ca lista se incarca direct filtrata (nu dead-end).
"""
if conn is None:
return templates.get_template("_acasa.html").render(
@@ -243,8 +241,8 @@ def _render_panel_import(request: Request) -> str:
def _render_panel_coada(request: Request, conn=None, account_id: int = 1) -> str:
"""US-003 (3.6): "coada" nu mai e panou propriu — serveste continutul Acasa
(Trimiterile sunt sectiune pe Acasa). Pastrat ca alias pentru deep-link/bookmark vechi."""
""""coada" nu mai e panou propriu — serveste continutul Acasa (Trimiterile sunt
sectiune pe Acasa). Pastrat ca alias pentru deep-link/bookmark vechi."""
return _render_panel_acasa(request, conn, account_id)
@@ -334,10 +332,10 @@ def _jurnal_context(
data_de: str | None = None, data_pana: str | None = None,
cont: str | None = None, page: int = 0,
) -> dict:
"""Context pentru tab-ul Jurnal (US-006): evenimente paginate + filtre + scope.
"""Context pentru tab-ul Jurnal: evenimente paginate + filtre + scope.
Admin -> vede TOT, cu filtru optional pe cont. Non-admin -> DOAR evenimentele
contului sau (regula NULL->cont 1, ca restul UI-ului). Decizie §5.
contului sau (regula NULL->cont 1, ca restul UI-ului).
"""
admin = is_account_admin(conn, account_id)
tip = (tip or "").strip() or None
@@ -397,7 +395,7 @@ def _jurnal_context(
def _render_panel_jurnal(request: Request, conn, account_id: int) -> str:
"""Randeaza panoul Jurnal ca string HTML (US-006)."""
"""Randeaza panoul Jurnal ca string HTML."""
return templates.get_template("_jurnal.html").render(_jurnal_context(request, conn, account_id))
@@ -424,20 +422,20 @@ def _render_panel_for_tab(request: Request, conn, account_id: int, tab: str, sta
@router.get("/", response_class=HTMLResponse)
def dashboard(request: Request, tab: str = "acasa", status: str | None = None) -> HTMLResponse:
"""Dashboard principal cu tab-uri (US-003).
"""Dashboard principal cu tab-uri.
Parametrul ?tab= permite deep-link pe orice sectiune; panoul activ e randat
server-side la full load (fara palpaiere la refresh, degradare gratiosa fara JS).
Tab invalid -> fallback la 'acasa'. `?status=` (US-014/T13) pre-filtreaza lista
Trimiteri de pe Acasa (deep-link din banner-ul "Necesita atentia ta").
Tab invalid -> fallback la 'acasa'. `?status=` pre-filtreaza lista Trimiteri de
pe Acasa (deep-link din banner-ul "Necesita atentia ta").
"""
account_id = require_login(request)
active_tab = tab if tab in _TABS_VALIDE else "acasa"
conn = get_connection()
try:
panel_html = _render_panel_for_tab(request, conn, account_id, active_tab, status=status)
# Badge contoare pe tab-uri (US-011): needs_mapping -> Mapari. Blocatele
# (fost badge "coada") se reflecta acum in heading-ul sectiunii Trimiteri (US-003).
# Badge contoare pe tab-uri: needs_mapping -> Mapari. Blocatele se reflecta in
# heading-ul sectiunii Trimiteri.
counts = _status_counts(conn, account_id)
badges = {
"mapari": counts.get("needs_mapping", 0),
@@ -460,7 +458,7 @@ def dashboard(request: Request, tab: str = "acasa", status: str | None = None) -
@router.get("/_fragments/acasa", response_class=HTMLResponse)
def fragment_acasa(request: Request) -> HTMLResponse:
"""Fragment HTMX pentru tab-ul Acasa (US-003, US-005)."""
"""Fragment HTMX pentru tab-ul Acasa."""
account_id = require_login(request)
conn = get_connection()
try:
@@ -472,16 +470,16 @@ def fragment_acasa(request: Request) -> HTMLResponse:
@router.get("/_fragments/import", response_class=HTMLResponse)
def fragment_import(request: Request) -> HTMLResponse:
"""Fragment HTMX pentru tab-ul Import — include zona de upload (US-003)."""
"""Fragment HTMX pentru tab-ul Import — include zona de upload."""
require_login(request)
return templates.TemplateResponse("_upload.html", _ctx(request))
@router.get("/_fragments/coada", response_class=HTMLResponse)
def fragment_coada(request: Request) -> HTMLResponse:
"""US-003 (3.6): "coada" nu mai are fragment propriu. Serveste continutul Acasa
(Trimiterile sunt sectiune permanenta pe Acasa) — evita un fragment `_coada.html`
orfan din bookmark-uri/HTMX vechi. Nu da 404."""
""""coada" nu mai are fragment propriu. Serveste continutul Acasa (Trimiterile sunt
sectiune permanenta pe Acasa) — evita un fragment `_coada.html` orfan din
bookmark-uri/HTMX vechi. Nu da 404."""
account_id = require_login(request)
conn = get_connection()
try:
@@ -528,7 +526,7 @@ def fragment_jurnal(
cont: str | None = None,
page: int = 0,
) -> HTMLResponse:
"""Tab Jurnal (US-006): evenimente app_events paginate + filtre, scoped pe cont.
"""Tab Jurnal: evenimente app_events paginate + filtre, scoped pe cont.
Admin vede tot (filtru optional pe cont); non-admin doar evenimentele proprii.
"""
@@ -563,8 +561,8 @@ def fragment_banner(request: Request) -> HTMLResponse:
def _blocate_defalcat(counts: dict[str, int]) -> list[tuple]:
"""Construieste lista [(eticheta, n), ...] pentru starile blocate cu n > 0.
Ordinea: needs_mapping, needs_data, error — aceeasi ca in PRD.
Returneaza lista goala daca nu exista nicio stare blocata.
Ordinea: needs_mapping, needs_data, error. Returneaza lista goala daca nu
exista nicio stare blocata.
"""
rezultat = []
for status in ("needs_mapping", "needs_data", "error"):
@@ -575,13 +573,12 @@ def _blocate_defalcat(counts: dict[str, int]) -> list[tuple]:
def _pills_categorii(counts: dict[str, int]) -> list[dict]:
"""Pill-uri pentru starile cu problema (US-003 PRD 5.10).
"""Pill-uri pentru starile cu problema.
Inlocuieste _blocate_actionabil (care incarca PII/VIN per rand).
Reutilizeaza contoarele deja calculate din _status_counts.
Returneza lista goala daca nu exista nicio stare blocata.
Reutilizeaza contoarele deja calculate din _status_counts (fara PII/VIN per rand).
Returneaza lista goala daca nu exista nicio stare blocata.
"""
# DESIGN.md §Componente: Lipsa cod = --warn (chihlimbar), celelalte categorii = --err (rosu).
# Lipsa cod = --warn (chihlimbar), celelalte categorii = --err (rosu).
# Culoarea e CSS variable name (nu clasa), injectata direct in style tag al pill-ului,
# pentru ca s-needs_mapping in base.html e tot --err (incorect pentru pill).
PILL_DEFS = [
@@ -598,7 +595,7 @@ def _pills_categorii(counts: dict[str, int]) -> list[dict]:
@router.get("/_fragments/status", response_class=HTMLResponse)
def fragment_status(request: Request) -> HTMLResponse:
"""Bara de status persistenta cu etichete umane (US-002, PRD 3.4).
"""Bara de status persistenta cu etichete umane.
Scoped pe contul sesiunii. Expune starea worker, legatura RAR, ultima
autentificare, contorii de coada si defalcarea blocatelor pe motiv.
@@ -623,7 +620,7 @@ def fragment_status(request: Request) -> HTMLResponse:
"request": request,
"worker_lbl": worker_lbl,
"rar_lbl": rar_lbl,
# Stari binare pentru bife accesibile (US-001 PRD 3.5): glifa + culoare
# Stari binare pentru bife accesibile: glifa + culoare
"worker_ok": worker_alive,
"rar_ok": rar_ok,
"eticheta_ultima_auth": ETICHETA_ULTIMA_AUTENTIFICARE_RAR,
@@ -657,9 +654,8 @@ def _iso_date_prefix(value: object) -> str | None:
Permite filtrarea dupa data_prestatie chiar daca valoarea contine ora/minut/secunda
(ex. '2026-06-20 14:35:07' sau '2026-06-20T14:35:07') — extrage portiunea de data
fara a exclude timestamp-urile (bug-ul fix US-001: _is_iso_date cerea len==10).
Valori care nu incep cu o data ISO valida (ex. '05.12.2024') intorc None si
sunt excluse din filtru — comportament actual pastrat.
fara a exclude timestamp-urile. Valori care nu incep cu o data ISO valida
(ex. '05.12.2024') intorc None si sunt excluse din filtru.
"""
s = str(value or "").strip()
if len(s) < 10:
@@ -673,16 +669,16 @@ def _iso_date_prefix(value: object) -> str | None:
# Stari care semnaleaza o problema ce necesita atentia operatorului. Eticheta umana
# scurta de pe rand (US-001, R1) e ne-goala DOAR pe acestea; pe queued/sending/sent e "".
# scurta de pe rand e ne-goala DOAR pe acestea; pe queued/sending/sent e "".
_STARI_CU_PROBLEMA = ("error", "needs_data", "needs_mapping")
def _eticheta_problema(status: str, motiv: str) -> str:
"""Eticheta umana scurta a problemei pentru randul de tabel (US-001, R1).
"""Eticheta umana scurta a problemei pentru randul de tabel.
Reutilizeaza `motiv` (motiv_uman, deja calculat in randul de view) si cade pe
`eticheta_scurta` cand motivul e gol — NU re-parseaza `rar_error` (R1: DRY, fara
al 3-lea decoder). Codul BRUT de catalog ramane doar pentru modal, nu pe rand.
`eticheta_scurta` cand motivul e gol — NU re-parseaza `rar_error` (DRY, fara al
3-lea decoder). Codul BRUT de catalog ramane doar pentru modal, nu pe rand.
Sir gol pe stari fara problema (queued/sending/sent); ne-gol pe error/needs_*.
Defensiv: motiv_uman nu arunca, iar starile cu problema au intotdeauna eticheta
@@ -694,13 +690,13 @@ def _eticheta_problema(status: str, motiv: str) -> str:
def _submission_row_view(r) -> dict:
"""Imbogateste un rand de submission cu campuri afisabile umane (US-003/US-004)."""
"""Imbogateste un rand de submission cu campuri afisabile umane."""
eticheta = eticheta_stare(r["status"])
motiv = motiv_uman(r["status"], r["rar_error"])
return {
"id": r["id"],
"status": r["status"],
# PRD 5.8 US-007/US-006: pill = eticheta scurta; textul lung ramane ca tooltip (title=).
# pill = eticheta scurta; textul lung ramane ca tooltip (title=).
"stare_scurt": eticheta_scurta(r["status"]),
"stare_text": eticheta[0],
"stare_css": eticheta[2],
@@ -708,15 +704,15 @@ def _submission_row_view(r) -> dict:
"id_prezentare": r["id_prezentare"],
"updated_at": format_data_rar(r["updated_at"]),
"motiv": motiv,
# US-001/R1: eticheta umana scurta a problemei sub pill (text, nu cod brut).
# eticheta umana scurta a problemei sub pill (text, nu cod brut).
"eticheta_problema": _eticheta_problema(r["status"], motiv),
# US-011: randurile blocate (error/needs_data/needs_mapping) sunt selectabile
# pentru stergere bulk; sent/sending/queued raman read-only (fara checkbox).
# randurile blocate (error/needs_data/needs_mapping) sunt selectabile pentru
# stergere bulk; sent/sending/queued raman read-only (fara checkbox).
"gestionabil": r["status"] in _GESTIONABILE_WEB,
}
_PAGE_SIZE = 25 # Marime pagina fixa (US-004 PRD 5.10)
_PAGE_SIZE = 25 # Marime pagina fixa
@router.get("/_fragments/submissions", response_class=HTMLResponse)
@@ -728,9 +724,9 @@ def fragment_submissions(
data_pana: str | None = None,
page: int = 1,
) -> HTMLResponse:
"""Tabel Trimiteri, scoped pe cont, cu filtre optionale si paginare (US-009, US-004).
"""Tabel Trimiteri, scoped pe cont, cu filtre optionale si paginare.
US-004 H1: totalul se calculeaza DIFERIT dupa tipul de filtru:
Totalul se calculeaza DIFERIT dupa tipul de filtru:
- FARA filtru Python (status-only / niciun filtru): SQL COUNT(*) + LIMIT/OFFSET
- CU filtru vehicul/data activ: fetch-all -> filtreaza Python -> total=len -> slice
SQL COUNT/LIMIT pe calea cu filtru Python ar da total gresit (taie inainte de filtru).
@@ -756,8 +752,8 @@ def fragment_submissions(
where_sql = " AND ".join(where)
if filtru_python:
# Calea B: fetch-all, filtreaza in Python, slice (US-004 H1)
# FARA LIMIT — altfel paginile >8 ar disparea silentios (bug PRD H1)
# Calea B: fetch-all, filtreaza in Python, slice.
# FARA LIMIT — altfel paginile >8 ar disparea silentios.
rows_db = conn.execute(
"SELECT id, status, id_prezentare, rar_status_code, rar_error, retry_count, "
f"updated_at, payload_json FROM submissions WHERE {where_sql} ORDER BY id DESC",
@@ -773,7 +769,7 @@ def fragment_submissions(
if vehicul_q not in hay:
continue
if data_de or data_pana:
# Extragem portiunea YYYY-MM-DD (US-001 fix).
# Extragem portiunea YYYY-MM-DD.
d_prefix = _iso_date_prefix(prez["data_prestatie"])
if d_prefix is None:
continue
@@ -785,7 +781,7 @@ def fragment_submissions(
total = len(view_all)
pages = max(1, math.ceil(total / _PAGE_SIZE)) if total > 0 else 1
page = max(1, min(page, pages)) # clamp H2
page = max(1, min(page, pages)) # clamp la nr. de pagini
offset = (page - 1) * _PAGE_SIZE
view = view_all[offset:offset + _PAGE_SIZE]
@@ -796,7 +792,7 @@ def fragment_submissions(
).fetchone()[0]
pages = max(1, math.ceil(total / _PAGE_SIZE)) if total > 0 else 1
page = max(1, min(page, pages)) # clamp H2
page = max(1, min(page, pages)) # clamp la nr. de pagini
offset = (page - 1) * _PAGE_SIZE
rows_db = conn.execute(
@@ -815,13 +811,13 @@ def fragment_submissions(
"rows": view,
"filtru_activ": filtru_activ,
"csrf_token": get_csrf_token(request),
# Paginare (US-004)
# Paginare
"total": total,
"page": page,
"pages": pages,
"page_start": page_start,
"page_end": page_end,
# Filtre curente pentru linkurile de paginare (pastreaza filtrele, H2)
# Filtre curente pentru linkurile de paginare (pastreaza filtrele)
"f_status": status or "",
"f_vehicul": vehicul_q or "",
"f_data_de": data_de or "",
@@ -835,17 +831,17 @@ def fragment_submissions(
conn.close()
# Stari ne-trimise blocate pe care le putem corecta inline (US-010).
# Stari ne-trimise blocate pe care le putem corecta inline.
_CORECTABILE = ("needs_data", "needs_mapping")
# US-006b: stari cu select editabil cod_prestatie (superset al _CORECTABILE: error
# primeste select in formularul /repune, nu in /corecteaza — fara schimbare de vehicle fields).
# Stari cu select editabil cod_prestatie (superset al _CORECTABILE: error primeste
# select in formularul /repune, nu in /corecteaza — fara schimbare de vehicle fields).
_EDITABILE_OP = ("needs_data", "needs_mapping", "error")
# Stari gestionabile prin lifecycle web (US-011): sterge / re-pune in coada.
# Stari gestionabile prin lifecycle web: sterge / re-pune in coada.
_GESTIONABILE_WEB = ("error", "needs_data", "needs_mapping")
def _render_submissions(request: Request, conn, account_id: int) -> HTMLResponse:
"""Re-randeaza lista Trimiteri (fara filtre) — folosit dupa actiuni bulk (US-011)."""
"""Re-randeaza lista Trimiteri (fara filtre) — folosit dupa actiuni bulk."""
scope_sql, scope_params = account_scope_clause(account_id)
rows = conn.execute(
"SELECT id, status, id_prezentare, rar_status_code, rar_error, retry_count, "
@@ -865,7 +861,7 @@ def _render_submissions(request: Request, conn, account_id: int) -> HTMLResponse
def _payload_form_values(payload_json) -> dict:
"""Valori brute pentru prefill-ul formularului de corectie (US-010)."""
"""Valori brute pentru prefill-ul formularului de corectie."""
try:
data = json.loads(payload_json) if payload_json else {}
if not isinstance(data, dict):
@@ -882,7 +878,7 @@ def _payload_form_values(payload_json) -> dict:
def _nemapate_pentru_submission(row, nomenclator: list[dict]) -> list[dict]:
"""Operatiile nemapate ale UNUI submission needs_mapping, cu sugestii fuzzy (PRD 5.7).
"""Operatiile nemapate ale UNUI submission needs_mapping, cu sugestii fuzzy.
Echivalentul `pending_unmapped` restrans la un singur rand: parseaza payload_json,
aduna prestatiile fara cod_prestatie (cu cod_op_service) si ataseaza sugestii din
@@ -921,12 +917,12 @@ def _detaliu_ctx(request: Request, row, *, message: str | None = None,
"""Context pentru _trimitere_detaliu.html dintr-un rand de submission.
`conn`+`account_id` (optional): cand sunt date si randul e needs_mapping, expune
`nemapate_inline` + `nomenclator` pentru maparea inline din panou (PRD 5.7).
`nemapate_inline` + `nomenclator` pentru maparea inline din panou.
"""
eticheta = eticheta_stare(row["status"])
nemapate_inline: list[dict] = []
nomenclator: list[dict] = []
# Variabila interna: nomenclatorul complet (incarcat pentru needs_mapping, refolosit pt US-006)
# Nomenclatorul complet, incarcat pentru needs_mapping si refolosit mai jos.
_nomenclator_complet: list[dict] = []
if conn is not None and row["status"] == "needs_mapping":
# Un singur SELECT pe nomenclator: il refolosim si pentru sugestii si pentru dropdown.
@@ -934,14 +930,14 @@ def _detaliu_ctx(request: Request, row, *, message: str | None = None,
nemapate_inline = _nemapate_pentru_submission(row, _nomenclator_complet)
nomenclator = _nomenclator_complet if nemapate_inline else []
# US-006/US-006b: nomenclator pentru selectul cod_prestatie — needs_data/needs_mapping (in
# formularul /corecteaza) + error (in formularul /repune). Refoloseste _nomenclator_complet
# daca e deja incarcat (needs_mapping), altfel incarca fresh.
# Nomenclator pentru selectul cod_prestatie — needs_data/needs_mapping (in formularul
# /corecteaza) + error (in formularul /repune). Refoloseste _nomenclator_complet daca
# e deja incarcat (needs_mapping), altfel incarca fresh.
nomenclator_rar: list[dict] = []
if conn is not None and row["status"] in _EDITABILE_OP:
nomenclator_rar = _nomenclator_complet if _nomenclator_complet else load_nomenclator(conn)
# US-006: cod_prestatie curent din prima prestatie (pentru pre-selectare in select)
# cod_prestatie curent din prima prestatie (pentru pre-selectare in select)
cod_prestatie_curent = ""
try:
_pd = json.loads(row["payload_json"] or "{}")
@@ -969,14 +965,14 @@ def _detaliu_ctx(request: Request, row, *, message: str | None = None,
"created_at": format_data_rar(row["created_at"]),
"updated_at": format_data_rar(row["updated_at"]),
"next_attempt_at": format_data_rar(row["next_attempt_at"]),
# randuri ne-trimise blocate sunt corectabile (US-010); sent/sending nu
# randuri ne-trimise blocate sunt corectabile; sent/sending nu
"editabil": row["status"] in _CORECTABILE,
# US-011: error/needs_data/needs_mapping pot fi sterse / re-puse in coada
# error/needs_data/needs_mapping pot fi sterse / re-puse in coada
"gestionabil": row["status"] in _GESTIONABILE_WEB,
# PRD 5.7: mapare inline (operatii nemapate ale acestui rand + nomenclator)
# mapare inline (operatii nemapate ale acestui rand + nomenclator)
"nemapate_inline": nemapate_inline,
"nomenclator": nomenclator,
# US-006: select cod_prestatie pentru stari editabile
# select cod_prestatie pentru stari editabile
"nomenclator_rar": nomenclator_rar,
"cod_prestatie_curent": cod_prestatie_curent,
"corectie_msg": message,
@@ -988,7 +984,7 @@ def _detaliu_ctx(request: Request, row, *, message: str | None = None,
def _fetch_submission_scoped(conn, account_id: int, submission_id: int):
"""Randul scoped pe cont sau None (404 cross-account, nu confirmam existenta — B3)."""
"""Randul scoped pe cont sau None (404 cross-account, nu confirmam existenta)."""
scope_sql, scope_params = account_scope_clause(account_id)
return conn.execute(
f"SELECT * FROM submissions WHERE id=? AND {scope_sql}",
@@ -996,14 +992,14 @@ def _fetch_submission_scoped(conn, account_id: int, submission_id: int):
).fetchone()
# Campuri afisate in detaliul trimiterii (panou dedicat US-004). payload_json e
# plaintext si se foloseste doar pentru campurile derivate (prezentare_din_payload).
# Campuri afisate in detaliul trimiterii. payload_json e plaintext si se foloseste
# doar pentru campurile derivate (prezentare_din_payload).
@router.get("/_fragments/trimitere/{submission_id}", response_class=HTMLResponse)
def fragment_trimitere_detaliu(request: Request, submission_id: int) -> HTMLResponse:
"""Detaliu complet al unei trimiteri, in panou dedicat (#trimitere-detaliu).
Scoped pe contul sesiunii: 404 daca randul nu exista SAU apartine altui cont
(acelasi mesaj, nu confirmam existenta — vezi B3/router.py).
(acelasi mesaj, nu confirmam existenta).
"""
account_id = require_login(request)
conn = get_connection()
@@ -1021,7 +1017,7 @@ def fragment_trimitere_detaliu(request: Request, submission_id: int) -> HTMLResp
@router.post("/trimitere/{submission_id}/mapeaza", response_class=HTMLResponse)
async def post_mapeaza_inline(request: Request, submission_id: int) -> HTMLResponse:
"""Mapare inline din panoul de detaliu (PRD 5.7): alege cod RAR pentru o operatie nemapata.
"""Mapare inline din panoul de detaliu: alege cod RAR pentru o operatie nemapata.
Reutilizeaza EXACT save_mapping + reresolve_account (ca tab-ul Mapari) — fara logica
noua de clasificare. Re-rezolva scoped pe batch-ul randului (canal API batch_id IS NULL
@@ -1108,10 +1104,9 @@ async def post_corectie_trimitere(request: Request, submission_id: int) -> HTMLR
if isinstance(val, str) and val.strip() != "":
content[camp] = val.strip()
# US-006: injectare cod_prestatie din form INAINTE de resolve_prestatii.
# Oglindeste validarea din post_mapeaza_inline (nomenclator check). Codul nou
# e injectat in prima prestatie (index 0); build_key il include in hash (CLAUDE.md
# invariant "build_key hashuieste cod_prestatie, idempotency.py:34").
# Injectare cod_prestatie din form INAINTE de resolve_prestatii. Oglindeste
# validarea din post_mapeaza_inline (nomenclator check). Codul nou e injectat in
# prima prestatie (index 0); build_key il include in hash.
_cod_raw = form.get("cod_prestatie")
cod_prestatie_form = (_cod_raw.strip().upper() if isinstance(_cod_raw, str) else "")
if cod_prestatie_form:
@@ -1143,7 +1138,7 @@ async def post_corectie_trimitere(request: Request, submission_id: int) -> HTMLR
resolved, unmapped = resolve_prestatii(content.get("prestatii"), mapping, valid_codes, text_rules)
content["prestatii"] = resolved
# US-010: telemetrie pentru itemii rezolvati prin regula text (calea corectie web).
# telemetrie pentru itemii rezolvati prin regula text (calea corectie web).
_emite_text_rule_hits(conn, account_id, row["id"], resolved)
# Canonicalizare (strip ".0" odometru, VIN/nr upper) INAINTE de validare si cheie.
@@ -1238,8 +1233,8 @@ async def post_corectie_trimitere(request: Request, submission_id: int) -> HTMLR
"_trimitere_detaliu.html",
_detaliu_ctx(request, row2, message="Corectat — randul a fost re-pus in coada."),
)
# PRD 5.9 US-003 (R5): pe succes, lista se reincarca (trimiteriChanged) si modalul
# se inchide (inchideModal). After-settle ca inchiderea sa urmeze swap-ul fragmentului.
# Pe succes, lista se reincarca (trimiteriChanged) si modalul se inchide
# (inchideModal). After-settle ca inchiderea sa urmeze swap-ul fragmentului.
resp.headers["HX-Trigger-After-Settle"] = "trimiteriChanged, inchideModal"
return resp
finally:
@@ -1247,26 +1242,26 @@ async def post_corectie_trimitere(request: Request, submission_id: int) -> HTMLR
# =========================================================================== #
# US-011 — Lifecycle trimiteri blocate din dashboard: sterge / re-pune in coada #
# Peste helper-ul US-009 (submissions_admin). CSRF enforce; scoped pe sesiune. #
# Lifecycle trimiteri blocate din dashboard: sterge / re-pune in coada. #
# Peste helper-ul submissions_admin. CSRF enforce; scoped pe sesiune. #
# =========================================================================== #
@router.post("/trimitere/{submission_id}/repune", response_class=HTMLResponse)
async def post_repune_trimitere(request: Request, submission_id: int) -> HTMLResponse:
"""Re-pune in coada un rand blocat (error/needs_data/needs_mapping) din dashboard.
US-006b: daca randul e in starea `error` si formularul contine `cod_prestatie`,
actualizeaza codul in payload, recalculeaza cheia de idempotency si re-pun in coada
direct (fara `requeue_submission`, care nu actualizeaza cheia). Scoped pe sesiune
(404 cross-account/inexistent, 409 sent/sending). Re-randeaza panoul de detaliu cu
starea noua + nudge `trimiteriChanged` pentru lista.
Daca randul e in starea `error` si formularul contine `cod_prestatie`, actualizeaza
codul in payload, recalculeaza cheia de idempotency si re-pune in coada direct (fara
`requeue_submission`, care nu actualizeaza cheia). Scoped pe sesiune (404
cross-account/inexistent, 409 sent/sending). Re-randeaza panoul de detaliu cu starea
noua + nudge `trimiteriChanged` pentru lista.
"""
account_id = require_login(request)
form = await request.form()
verify_csrf(request, str(form.get("csrf_token") or ""))
conn = get_connection()
try:
# US-006b: prelucrare cod_prestatie pentru starea error (inaintea requeue_submission
# Prelucrare cod_prestatie pentru starea error (inaintea requeue_submission
# standard, care nu actualizeaza cheia de idempotency).
_cod_raw = form.get("cod_prestatie")
cod_prestatie_form = (_cod_raw.strip().upper() if isinstance(_cod_raw, str) else "")
@@ -1398,7 +1393,7 @@ async def post_sterge_trimitere(request: Request, submission_id: int) -> HTMLRes
resp = HTMLResponse(
'<div class="flash" style="margin:0;">Trimitere stearsa.</div>'
)
# PRD 5.9 US-003 (R5): pe succes, lista se reincarca + modalul se inchide.
# Pe succes, lista se reincarca + modalul se inchide.
resp.headers["HX-Trigger-After-Settle"] = "trimiteriChanged, inchideModal"
return resp
finally:
@@ -1410,7 +1405,7 @@ async def post_sterge_bulk(request: Request) -> HTMLResponse:
"""Sterge in bloc trimiterile selectate (doar blocate, scoped pe sesiune).
Sare peste randuri sent/sending (read-only) si cross-account (inexistente) fara a
opri operatia — pe modelul panoului admin (PRD 5.5). Re-randeaza lista Trimiteri.
opri operatia — pe modelul panoului admin. Re-randeaza lista Trimiteri.
"""
account_id = require_login(request)
form = await request.form()
@@ -1436,7 +1431,7 @@ async def post_sterge_bulk(request: Request) -> HTMLResponse:
def _load_saved_op_mappings(conn, account_id: int) -> list[dict]:
"""Mapari operatie->cod salvate (operations_mapping) ale contului, cu numele
prestatiei jonctionat din nomenclator (US-005). Scoped pe cont (NOT NULL → simplu)."""
prestatiei jonctionat din nomenclator. Scoped pe cont (NOT NULL → simplu)."""
acct = account_or_default(account_id)
rows = conn.execute(
"SELECT o.id, o.cod_op_service, o.cod_prestatie, o.auto_send, n.nume_prestatie "
@@ -1458,7 +1453,7 @@ def _load_saved_op_mappings(conn, account_id: int) -> list[dict]:
def _load_column_formats(conn, account_id: int) -> list[dict]:
"""Formate de coloane salvate (column_mappings) ale contului (US-006).
"""Formate de coloane salvate (column_mappings) ale contului.
Coloanele afisate = cheile din json_mapare (campurile recunoscute). Scoped pe cont.
"""
@@ -1507,7 +1502,7 @@ def _render_mapari(
def fragment_mapari(request: Request) -> HTMLResponse:
"""Editor mapari: operatii ROAAUTO nemapate + sugestii fuzzy pe nomenclator RAR.
Scoped pe contul sesiunii (C6/task#7): pending_unmapped primeste account_id explicit.
Scoped pe contul sesiunii: pending_unmapped primeste account_id explicit.
"""
account_id = require_login(request)
conn = get_connection()
@@ -1547,7 +1542,7 @@ def post_mapare(
# =========================================================================== #
# US-005 — Mapari operatii salvate: editare cod/auto-send + stergere #
# Mapari operatii salvate: editare cod/auto-send + stergere. #
# CRUD pe operations_mapping scoped pe sesiune; re-rezolva blocatele la edit. #
# =========================================================================== #
@@ -1611,7 +1606,7 @@ def post_sterge_mapare_salvata(
# =========================================================================== #
# US-004 (5.8) — Reguli automate (text): substring -> cod RAR #
# Reguli automate (text): substring -> cod RAR. #
# Adaugare/stergere reguli text scoped pe sesiune; salvarea re-rezolva blocajele.#
# =========================================================================== #
@@ -1628,7 +1623,7 @@ def post_salveaza_regula_text(
Scoped pe contul sesiunii (save_text_rule foloseste account_or_default(sesiune)).
Valideaza cod_prestatie fata de nomenclator INAINTE de save (cod necunoscut ->
respins inline, fara salvare). La succes: mesaj „Regula salvata. Deblocate: N"
+ trigger trimiteriChanged (refresh lista), ca maparea inline (5.7).
+ trigger trimiteriChanged (refresh lista), ca maparea inline.
"""
account_id = require_login(request)
verify_csrf(request, csrf_token)
@@ -1647,7 +1642,7 @@ def post_salveaza_regula_text(
request, conn, account_id,
message=f"Cod RAR necunoscut in nomenclator: {cod}.",
)
# US-011: avertisment neblocant daca regula noua se suprapune (substring,
# avertisment neblocant daca regula noua se suprapune (substring,
# oricare directie) cu una existenta. Calculam INAINTE de save, fata de
# regulile curente, ca pattern-ul nou sa nu se compare cu sine.
overlap = text_rules_overlap(pat, load_text_rules(conn, account_id))
@@ -1696,7 +1691,7 @@ def post_preview_regula_text(
pattern: str = Form(""),
csrf_token: str | None = Form(None),
) -> HTMLResponse:
"""Preview pre-salvare (US-009): cate operatii nemapate ar potrivi regula.
"""Preview pre-salvare: cate operatii nemapate ar potrivi regula.
NU salveaza nimic (zero scriere DB). Normalizeaza pattern-ul cu
normalize_for_match, numara operatiile DISTINCTE nemapate ale contului
@@ -1743,7 +1738,7 @@ def post_preview_regula_text(
# =========================================================================== #
# US-006 — Formate de coloane salvate: editare format data + stergere #
# Formate de coloane salvate: editare format data + stergere. #
# CRUD pe column_mappings scoped pe sesiune (prin id, verificat pe account). #
# =========================================================================== #
@@ -1879,7 +1874,7 @@ def _web_compute_preview(
if not raw_rows_db:
return "Niciun rand in batch."
# Decripteaza randurile + override-urile editate (3.6)
# Decripteaza randurile + override-urile editate
rows: list[dict[str, Any]] = []
overrides: list[dict[str, Any]] = []
for r in raw_rows_db:
@@ -1907,12 +1902,12 @@ def _web_compute_preview(
json_mapare: dict[str, str] = json.loads(mapping_row["json_mapare"])
format_data: str | None = mapping_row["format_data"]
# Mapare operatii (o singura incarcare — Eng#5)
# Mapare operatii (o singura incarcare)
mapping_meta = load_mapping_meta(conn, acct)
mapping = {op: meta["cod_prestatie"] for op, meta in mapping_meta.items()}
# US-010/US-003 (paritate): preview-ul web trebuie sa aplice ACELEASI reguli text +
# validare nomenclator ca si commit-ul (2426), altfel un rand rezolvabil doar prin
# regula text ar fi marcat needs_mapping si exclus din commit. Incarcate o data.
# Paritate cu commit-ul: preview-ul web trebuie sa aplice ACELEASI reguli text +
# validare nomenclator, altfel un rand rezolvabil doar prin regula text ar fi marcat
# needs_mapping si exclus din commit. Incarcate o data.
valid_codes = load_nomenclator_codes(conn) or None
text_rules = load_text_rules(conn, acct)
@@ -1980,7 +1975,7 @@ def _web_compute_preview(
"idempotency_key": key,
})
# Already_sent: batch lookup (Eng#5 — fara N+1)
# Already_sent: batch lookup (fara N+1)
unique_keys = list(set(keys_for_lookup))
already_sent_map = _already_sent_lookup(conn, account_id, unique_keys)
@@ -2091,7 +2086,7 @@ async def web_upload_import(
try:
sig = _signature(parsed.columns)
# Stagingul in DB (tranzactie explicita — Issue 6)
# Stagingul in DB (tranzactie explicita)
conn.execute("BEGIN IMMEDIATE")
try:
cur = conn.execute(
@@ -2330,15 +2325,15 @@ def web_preview_import(
# =========================================================================== #
# US-002 (3.6) — Editare celule in preview: mod editare pe rand. #
# Swap pe rand (#preview-row-N) + OOB contoare, NU pe #import-section (D-3.1). #
# Status rederivat DOAR prin _resolve_row_for_preview (H2 — fara clasificator). #
# Editare celule in preview: mod editare pe rand. #
# Swap pe rand (#preview-row-N) + OOB contoare, NU pe #import-section. #
# Status rederivat DOAR prin _resolve_row_for_preview (fara clasificator). #
# =========================================================================== #
def _preview_one_row(conn, import_id: int, account_id: int, row_index: int):
"""Recalculeaza preview-ul si extrage un singur rand.
Statusul e rederivat prin `_resolve_row_for_preview` (H2, fara clasificator duplicat),
Statusul e rederivat prin `_resolve_row_for_preview` (fara clasificator duplicat),
iar `_web_compute_preview` persista `resolved_status` pentru toate randurile — astfel
confirmarea (commit) vede starea editata. Intoarce (result, row) sau (mesaj, None)."""
result = _web_compute_preview(conn, import_id, account_id)
@@ -2366,7 +2361,7 @@ def _render_preview_rand(
@router.get("/_import/{import_id}/rand/{row_index}/editare", response_class=HTMLResponse)
def web_rand_editare(request: Request, import_id: int, row_index: int) -> HTMLResponse:
"""Intra in mod editare pe un rand de preview (randul devine FORM propriu, D-3.3)."""
"""Intra in mod editare pe un rand de preview (randul devine FORM propriu)."""
account_id = require_login(request)
conn = get_connection()
try:
@@ -2400,11 +2395,11 @@ def web_rand_display(request: Request, import_id: int, row_index: int) -> HTMLRe
@router.post("/_import/{import_id}/rand/{row_index}/editeaza", response_class=HTMLResponse)
async def web_editeaza_rand(request: Request, import_id: int, row_index: int) -> HTMLResponse:
"""Alias web (US-001/US-002): persista override (mutatie pura) + re-randeaza DOAR randul.
"""Persista override (mutatie pura) + re-randeaza DOAR randul.
Statusul e rederivat prin `_resolve_row_for_preview` (H2). Swap pe rand + OOB
contoare (D-3.1). Daca raman erori de continut pe camp, randul ramane in editare
cu valorile pastrate si mesajul pe campul vinovat (D-2.1/D-2.2)."""
Statusul e rederivat prin `_resolve_row_for_preview`. Swap pe rand + OOB contoare.
Daca raman erori de continut pe camp, randul ramane in editare cu valorile pastrate
si mesajul pe campul vinovat."""
account_id = require_login(request)
form = await request.form()
verify_csrf(request, str(form.get("csrf_token") or ""))
@@ -2502,8 +2497,8 @@ async def web_confirma_import(
Replica logica din import_router.commit_import dar cu input din form HTML
si raspuns HTML (nu JSON). INSERT per-rand ON CONFLICT DO NOTHING (TOCTOU).
C8/OV-2: account_id din sesiune propagat consecvent la build_key si toate lookup-urile.
C12: require_login — pe scrieri NICIODATA fallback cont 1 in prod.
account_id din sesiune propagat consecvent la build_key si toate lookup-urile.
require_login — pe scrieri NICIODATA fallback cont 1 in prod.
"""
account_id = require_login(request)
acct = account_or_default(account_id)
@@ -2644,11 +2639,11 @@ async def web_confirma_import(
# Mapare operatii
mapping_meta = load_mapping_meta(conn, acct)
mapping_ops = {op: meta["cod_prestatie"] for op, meta in mapping_meta.items()}
# T2: validare nomenclator + reguli text incarcate O DATA, inainte de bucla pe randuri.
# validare nomenclator + reguli text incarcate O DATA, inainte de bucla pe randuri.
valid_codes = load_nomenclator_codes(conn) or None
text_rules = load_text_rules(conn, acct)
# Enqueue in tranzactie explicita (Issue 6) — INSERT ON CONFLICT DO NOTHING (TOCTOU)
# Enqueue in tranzactie explicita — INSERT ON CONFLICT DO NOTHING (TOCTOU)
enqueued: list[dict] = []
toctou: list[int] = []
rows_for_hash: list[str] = []
@@ -2696,7 +2691,7 @@ async def web_confirma_import(
"odometru_final": canon["odometru_final"],
})
# Override editat in preview (3.6) — aplicat ULTIMUL, ca in resolver.
# Override editat in preview — aplicat ULTIMUL, ca in resolver.
override = item.get("override") or {}
if override:
mapped.update(override)
@@ -2729,7 +2724,7 @@ async def web_confirma_import(
if cur.rowcount == 0:
toctou.append(row_index)
else:
# US-010: telemetrie pentru itemii rezolvati prin regula text.
# telemetrie pentru itemii rezolvati prin regula text.
_emite_text_rule_hits(conn, acct, int(cur.lastrowid), resolved_p)
enqueued.append({"submission_id": cur.lastrowid, "row_index": row_index})
@@ -2740,7 +2735,7 @@ async def web_confirma_import(
n_enqueued = len(enqueued)
# Log atestare (Voce#9)
# Log atestare
rows_hash = hashlib.sha256(
json.dumps(sorted(rows_for_hash), ensure_ascii=False).encode("utf-8")
).hexdigest() if rows_for_hash else ""
@@ -2757,7 +2752,7 @@ async def web_confirma_import(
# Succes → bara de upload slim cu mesaj de confirmare. are_trimiteri=True:
# contul tocmai a pus randuri in coada -> bara ramane slim si dezvaluie
# sectiunea "Trimiterile tale" de pe Acasa (US-003/US-004).
# sectiunea "Trimiterile tale" de pe Acasa.
toctou_msg = f" ({len(toctou)} coliziuni TOCTOU excluse)" if toctou else ""
return templates.TemplateResponse("_upload.html", _ctx(
request,
@@ -2773,8 +2768,8 @@ async def web_confirma_import(
# =========================================================================== #
# US-007 — Sectiune "Contul meu": rotire cheie API + creds RAR din UI #
# Rute web proprii scoped pe sesiune (C13: nu reutilizeaza /v1/conturi/rar-creds
# Sectiune "Contul meu": rotire cheie API + creds RAR din UI. #
# Rute web proprii scoped pe sesiune (nu reutilizeaza /v1/conturi/rar-creds #
# care cere cheie API; sesiunea web e suficienta ca identitate). #
# =========================================================================== #
@@ -2846,10 +2841,9 @@ def integrare_test_cheie(
) -> HTMLResponse:
"""Verifica cheia API lipita de utilizator — scoped pe contul sesiunii.
US-004 (PRD Etapa 5): permite utilizatorului sa confirme ca o cheie copiata
din generatorul de exemple corespunde contului sau, fara efecte secundare
(fara creare/rotire). Cheie goala, invalida sau a altui cont -> mesaj de
eroare neutru (fara eco al cheii in raspuns).
Permite utilizatorului sa confirme ca o cheie copiata din generatorul de exemple
corespunde contului sau, fara efecte secundare (fara creare/rotire). Cheie goala,
invalida sau a altui cont -> mesaj de eroare neutru (fara eco al cheii in raspuns).
"""
account_id = require_login(request)
verify_csrf(request, csrf_token)
@@ -2901,7 +2895,7 @@ def cont_rar_creds(
rar_parola: str = Form(""),
csrf_token: str | None = Form(None),
) -> HTMLResponse:
"""Seteaza creds RAR per cont din sesiune (ruta web proprie, C13).
"""Seteaza creds RAR per cont din sesiune (ruta web proprie).
Camp parola NICIODATA re-pus in value= la re-randare.
Validare minima: email si parola negoale.