feat(errors): erori pe 3 niveluri (problema+cauza+fix) pe API si UI (PRD 5.4)

Catalog central pur app/errors.py ca sursa unica cod->{problema,fix},
consumat de API+UI+worker. Aditiv (field/message pastrate la octet) +
rar_error stocat superset. Scope: fluxul de declarare; login/signup/CSRF
neatinse. labels.parse_erori degradeaza gratios; UI progresiv AA light+dark.
631 teste.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-06-23 10:28:09 +00:00
parent b48501d8e4
commit 14e1c463f0
25 changed files with 2440 additions and 44 deletions

View File

@@ -23,6 +23,7 @@ from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
from .. import __version__
from .. import errors as _errors
from ..auth import rotate_api_key
from ..payload_view import prezentare_din_payload
from ..web.csrf import get_csrf_token, verify_csrf
@@ -33,6 +34,7 @@ from .labels import (
eticheta_worker,
format_data_rar,
motiv_uman,
parse_erori,
)
from ..web.session import require_login
from ..api.v1.import_router import (
@@ -71,6 +73,8 @@ _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)
templates.env.globals["parse_erori"] = parse_erori
_BLOCKED = ("error", "needs_data", "needs_mapping")
@@ -604,6 +608,7 @@ def _detaliu_ctx(request: Request, row, *, message: str | None = None,
"rar_status_code": row["rar_status_code"],
"rar_error": row["rar_error"],
"motiv": motiv_uman(row["status"], row["rar_error"]),
"erori_3n": parse_erori(row["rar_error"]),
"retry_count": row["retry_count"],
"created_at": format_data_rar(row["created_at"]),
"updated_at": format_data_rar(row["updated_at"]),
@@ -1279,13 +1284,25 @@ async def web_upload_import(
except MultipleSheets as ms:
return templates.TemplateResponse("_upload.html", _ctx(request, sheets=ms.sheet_names))
except FileTooLarge as e:
return templates.TemplateResponse("_upload.html", _ctx(request, error=str(e)))
eroare_upload = _errors.eroare("IMPORT_FISIER_PREA_MARE", cauza=str(e))
return templates.TemplateResponse("_upload.html", _ctx(
request, error=str(e), eroare_upload=eroare_upload
))
except HeaderError as e:
return templates.TemplateResponse("_upload.html", _ctx(request, error=f"Antet neclar: {e}"))
eroare_upload = _errors.eroare("IMPORT_ANTET_NECLAR", cauza=f"Antet neclar: {e}")
return templates.TemplateResponse("_upload.html", _ctx(
request, error=f"Antet neclar: {e}", eroare_upload=eroare_upload
))
except UnicodeDecodeError as e:
return templates.TemplateResponse("_upload.html", _ctx(request, error=f"Encoding nesuportat: {e.reason}"))
eroare_upload = _errors.eroare("IMPORT_ENCODING", cauza=f"Encoding nesuportat: {e.reason}")
return templates.TemplateResponse("_upload.html", _ctx(
request, error=f"Encoding nesuportat: {e.reason}", eroare_upload=eroare_upload
))
except Exception as e:
return templates.TemplateResponse("_upload.html", _ctx(request, error=f"Fisier nerecunoscut (xlsx/csv): {type(e).__name__}"))
eroare_upload = _errors.eroare("IMPORT_FISIER_NERECUNOSCUT", cauza=f"Fisier nerecunoscut (xlsx/csv): {type(e).__name__}")
return templates.TemplateResponse("_upload.html", _ctx(
request, error=f"Fisier nerecunoscut (xlsx/csv): {type(e).__name__}", eroare_upload=eroare_upload
))
conn = get_connection()
try:
@@ -1370,6 +1387,52 @@ async def web_save_mapare_coloane(
account_id = require_login(request)
acct = account_or_default(account_id)
# Detecta body JSON trimis eronat (Content-Type: application/json) → COLOANE_FORMAT_JSON
content_type = request.headers.get("content-type", "")
if "application/json" in content_type:
body = await request.body()
try:
json.loads(body)
# JSON valid dar trimis pe ruta de form — tot e format gresit pentru aceasta ruta
except (json.JSONDecodeError, ValueError):
pass
# Indiferent daca JSON-ul e valid sau nu, Content-Type application/json e gresit pentru ruta form
eroare_mapare = _errors.eroare(
"COLOANE_FORMAT_JSON",
cauza="Cererea a fost trimisa ca JSON (application/json) in loc de form data.",
)
conn = get_connection()
try:
first_row = conn.execute(
"SELECT raw_json FROM import_rows WHERE batch_id=? ORDER BY row_index LIMIT 1",
(import_id,),
).fetchone()
columns: list[str] = []
if first_row:
try:
rd = decrypt_creds(first_row["raw_json"]) or {}
columns = list(rd.keys())
except Exception:
pass
fuzzy: dict[str, list[dict]] = {}
for col in columns:
sugg = _fuzzy_suggest_column(col, limit=3)
if sugg:
fuzzy[col] = sugg
return templates.TemplateResponse("_mapcoloane.html", _ctx(
request,
import_id=import_id,
columns=columns,
sample_rows=[],
fuzzy_suggestions=fuzzy,
canonical_fields=_CANONICAL_FIELDS,
format_data=None,
eroare_mapare=eroare_mapare,
error=True,
))
finally:
conn.close()
form = await request.form()
# Colectare perechi coloana fisier → camp canonic din form