diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000..bd34ec8 --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,150 @@ +# DESIGN.md — Sistem de design AutoPass (by ROMFAST) + +> Sursa de adevar pentru identitatea vizuala a dashboard-ului. Implementarea concreta sta in +> `app/web/templates/base.html` (variabile CSS `:root` + `[data-theme="light"]`). Acest fisier +> spune *ce* si *de ce*; base.html spune *cum*. + +## Lucrul de retinut + +> „Software serios pentru o obligatie legala serioasa — dar parte din familia ROMFAST/ROA, nu un +> tool anonim." Operatorul de service trebuie sa simta ca declara la stat printr-un instrument de +> incredere, cu identitatea producatorului (ROMFAST) prezenta discret, nu griul generic de azi. + +## Context produs + +Gateway web care declara prezentari de service-auto la RAR AUTOPASS (L.142/2023). Utilizatori: +operatori de service-auto si integratori ROAAUTO. Face parte din familia **ROA — Romfast +Applications** (ERP romanesc, modul Service Auto). Referinta de brand: **romfast.ro** — alb curat, +accent albastru azur, pill-uri rotunjite, comutator de tema, logo rosu+albastru. + +## Decizie cromatica + +Accentul functional = **albastrul ROMFAST** (acelasi cu „FAST" din logo si cu accentul de pe +romfast.ro), nu albastrul generic SaaS de pana acum. Rosul apare DOAR in wordmark-ul „ROM" — nu ca +accent de UI, fiindca rosul e rezervat starilor de eroare. Un singur accent, restul neutre, ca +sistemul sa ramana discret. + +### Paleta — Dark (default) + +``` +--bg: #0f1218 fundal aplicatie +--card: #181c24 suprafete (carduri, modal, inputuri pe fundal) +--ink: #e6e9ef text principal +--muted: #8b93a7 text secundar (label-uri, coduri, „by") +--line: #262b36 borduri, separatoare +--accent:#2E74D6 azur ROMFAST — butoane primare, pill activ, linkuri, focus +--ok: #2FBF8F sent / succes +--warn: #E0A93B sending / atentie / Lipsa cod +--err: #E05D5D error / needs_data / Date incomplete +``` + +### Paleta — Light (`[data-theme="light"]`) + +``` +--bg: #f5f7fa fundal (alb-rece ca romfast.ro) +--card: #ffffff suprafete +--ink: #1a1d24 text principal +--muted: #5c6473 text secundar +--line: #e2e5ea borduri +--accent:#1F66C9 azur, variantă mai inchisa pentru contrast AA pe alb +--ok: #15803d verde AA pe alb +--warn: #b45309 chihlimbar AA pe alb +--err: #dc2626 rosu AA pe alb +``` + +### Paleta — Petrol (`[data-theme="petrol"]`, tema selectabila) + +Tema intunecata alternativa, cu accent petrol-teal (directia initiala aleasa, pastrata ca optiune). +Aceleasi neutre-calde inchise; doar accentul difera de azur. + +``` +--bg: #0e1416 fundal petrol-inchis +--card: #161e20 suprafete +--ink: #e6e9ef text principal +--muted: #8b93a7 text secundar +--line: #232c2e borduri +--accent:#0E7C7B teal petrol — butoane, pill activ, linkuri, focus +--ok: #2FBF8F sent +--warn: #E0A93B atentie +--err: #E05D5D eroare +``` + +### Culori de brand (doar wordmark, NU variabile de UI) + +``` +ROM: #D1342F rosu logo +FAST: #2E74D6 albastru logo (= accentul de UI in dark) +``` + +Contrast: textul principal pe fundal ramane AA in ambele teme; accentul pe alb foloseste varianta +mai inchisa (`#1F66C9`) ca text/linkul sa treaca AA. + +## Tipografie + +- **UI / titluri**: **IBM Plex Sans** — sans-serif cu caracter ingineresc, open-source, potrivit + pentru „software serios", parte din limbajul vizual tehnic. Fallback: `system-ui, sans-serif`. +- **Coduri / monospace**: **IBM Plex Mono** — pentru coduri RAR (REV2), VIN, numar inmatriculare, + detalii tehnice. Inlocuieste `ui-monospace/Menlo` actual cu o familie coerenta cu UI-ul. +- **Incarcare**: self-host `woff2` (subset latin + latin-ext pentru diacritice romanesti) in + `app/web/static/fonts/`, `font-display: swap`. Fara CDN extern (gateway intern, fara dependente + de retea la runtime). Pana la self-host, fallback la stiva de sistem nu strica layout-ul. + +## Header & branding + +- Titlul „Gateway RAR AUTOPASS" **centrat** pe header. +- Sub titlu, mic: **logo-ul ROMFAST** (`/static/romfast_logo.png`, ~28px inaltime). Decizie user + (2026-06-25, US-012b): se foloseste PNG-ul real al logo-ului (ROM rosu + FAST albastru, fundal + transparent — lizibil pe light/dark/petrol), NU wordmark-ul text. Wordmark-ul text (`by ROM FAST` + cu `ROM #D1342F` / `FAST #2E74D6`) ramane documentat ca alternativa, dar livrabila finala + foloseste imaginea. +- Controalele (comutator tema, versiune, hamburger ☰) raman la **dreapta**, fara a strica + centrarea optica a titlului (ex. grila 3 coloane: stanga goala/echilibru, centru titlu, dreapta + controale). +- Responsiv: pe mobil, wordmark-ul ramane sub titlu; controalele nu se suprapun (degrada elegant, + eventual titlu mai mic). + +## Selector de tema + +Inlocuieste comutatorul binar soare/luna cu un **buton ciclic** (pattern ca demoanaf.ro): un +singur buton care roteste la fiecare click prin setul de teme, cu iconita + tooltip/`aria-label` +care arata tema curenta („Tema: Light" etc.). + +Ordinea ciclului: **Light → Dark → Petrol → Auto → (inapoi la Light)**. + +- `Light` → `data-theme="light"` (azur pe alb) +- `Dark` → `data-theme="dark"` (azur pe inchis, comportamentul implicit actual) +- `Petrol` → `data-theme="petrol"` (teal pe petrol-inchis) +- `Auto` → urmeaza `prefers-color-scheme`; rezolva la Light azur sau Dark azur in functie de OS + (nu seteaza `data-theme` fix, ci il deriva la paint). + +Persistenta: preferinta explicita (inclusiv „Auto") in `localStorage`, doar la click. Scriptul +anti-FOUC din `` trebuie sa rezolve „Auto"→light/dark inainte de primul paint (fara blink). +Iconite: ☀ Light, ☾ Dark, ◐ Petrol, ◉ Auto. Default la prima vizita = Auto (OS-aware), ca azi. + +## Componente — note de aplicare + +- **Pill-uri de stare/filtru**: rotunjite (`border-radius:99px`), ca badge-ul „ROA" de pe + romfast.ro. Pill activ = fundal accent discret (`color-mix(in srgb, var(--accent) ...)`), text + pe accent. Categoriile de problema isi pastreaza registrul: Date incomplete/Eroare = `--err`, + Lipsa cod = `--warn`. +- **Butoane primare**: fundal `--accent`, text alb (neschimbat ca structura, doar culoarea noua). +- **Linkuri / sugestii**: `--accent`. +- **Focus**: `outline:2px solid var(--accent)` (deja folosit pe randuri). +- **Suprafete de stare** (banner, flash, eroare-3n): raman pe `color-mix` peste `--err/--warn/--ok`, + deci se adapteaza automat la noua paleta si la light/dark. + +## Ce NU schimbam + +- Mecanismul light/dark existent (anti-FOUC, persistenta `localStorage`, comutator) — il pastram, + doar reimprospatam variabilele. +- Nu introducem rosu ca accent de UI (conflict cu eroare). +- ~~Nu folosim PNG-ul logo cu efect 3D in interfata (wordmark redat ca text).~~ REVIZUIT + (decizie user 2026-06-25): logo-ul PNG real e folosit in header (US-012b). Fundal transparent + + culori proprii il fac lizibil pe toate temele; nu aplicam filtre. +- Nu adaugam un al doilea accent — sistemul ramane monocrom-accent + neutre. + +## Legatura cu implementarea (PRD 5.10) + +US-012 (header „by ROMFAST" + titlu centrat) si US-013 (paleta) din +`docs/prd/prd-5.10-ux-filtre-pill-paginare-mapari-meniu.md` implementeaza acest sistem. Valorile de +mai sus sunt sursa pentru variabilele din `base.html`. diff --git a/app/payload_view.py b/app/payload_view.py index 592c090..403f720 100644 --- a/app/payload_view.py +++ b/app/payload_view.py @@ -115,6 +115,16 @@ def prezentare_din_payload(payload: str | dict | None) -> dict[str, str]: # cod_rar: exclusiv din cod_prestatie (NU fallback la cod_op_service); uppercase + strip ".0" cod_rar = _clean_cod_rar(item.get("cod_prestatie")) + # US-002: operatia de service originala (codul intern + denumire venita prin API/import), + # distincta de operatia RAR mapata (cod_rar). + # Conventie goala: aceste campuri NOI intorc "" (string gol) cand lipsesc — NU EMPTY="—". + # Motivul: US-007 decide sa nu afiseze randul deloc (vs afisaj gol), testând `!= ""`. + # Campurile vechi (vehicul_nr, vin, operatie etc.) pastreaza conventia EMPTY="—". + op_service_cod = _clean_str(item.get("cod_op_service")) + # op_service_denumire e relevant doar cand exista un cod de operatie de service; + # altfel ar expune denumirea RAR drept op. de service, ceea ce e semantic incorect. + op_service_denumire = _clean_str(item.get("denumire")) if op_service_cod else "" + return { "vehicul_nr": nr or EMPTY, "vin": vin or EMPTY, @@ -124,4 +134,7 @@ def prezentare_din_payload(payload: str | dict | None) -> dict[str, str]: "odometru": odo or EMPTY, "cod": cod or EMPTY, "cod_rar": cod_rar or EMPTY, + # US-002: chei noi cu conventie goala "" (nu EMPTY) — vezi comentariu de mai sus + "op_service_cod": op_service_cod, + "op_service_denumire": op_service_denumire, } diff --git a/app/web/routes.py b/app/web/routes.py index df4678a..d2a16c8 100644 --- a/app/web/routes.py +++ b/app/web/routes.py @@ -13,6 +13,7 @@ from __future__ import annotations import hashlib import json +import math import sqlite3 from datetime import datetime, timezone from pathlib import Path @@ -555,43 +556,26 @@ def _blocate_defalcat(counts: dict[str, int]) -> list[tuple]: return rezultat -# Cate randuri blocate identificam nominal sub fiecare categorie din banner (US-014). -_BLOCATE_SAMPLE = 3 +def _pills_categorii(counts: dict[str, int]) -> list[dict]: + """Pill-uri pentru starile cu problema (US-003 PRD 5.10). - -def _blocate_actionabil(conn, account_id: int) -> list[dict]: - """Categorii blocate cu identificatorii primelor randuri + deep-link (US-014). - - Pentru fiecare stare blocata cu n>0: eticheta umana, contorul, primii N identificatori - (VIN partial + nr inmatriculare + #id — PII doar partial, ca jurnalul) si cati raman. - Scoped pe cont (regula NULL->1). Lista goala -> banner-ul nu se randeaza (se stinge). + 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. """ - from ..security import vin_partial - scope_sql, scope_params = account_scope_clause(account_id) - out: list[dict] = [] - for status in ("needs_mapping", "needs_data", "error"): - rows = conn.execute( - f"SELECT id, payload_json FROM submissions WHERE {scope_sql} AND status=? ORDER BY id DESC", - scope_params + [status], - ).fetchall() - if not rows: - continue - sample = [] - for r in rows[:_BLOCATE_SAMPLE]: - prez = prezentare_din_payload(r["payload_json"]) - sample.append({ - "id": r["id"], - "vin": vin_partial(prez.get("vin") or ""), - "nr": prez.get("vehicul_nr") or "", - }) - out.append({ - "status": status, - "eticheta": eticheta_stare(status), - "n": len(rows), - "randuri": sample, - "rest": max(0, len(rows) - len(sample)), - }) - return out + # DESIGN.md §Componente: 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 = [ + ("needs_mapping", "Lipsa cod", "--warn"), + ("needs_data", "Date incomplete", "--err"), + ("error", "Eroare", "--err"), + ] + return [ + {"status": status, "label": label, "color_var": color_var, "n": counts.get(status, 0)} + for status, label, color_var in PILL_DEFS + if counts.get(status, 0) > 0 + ] @router.get("/_fragments/status", response_class=HTMLResponse) @@ -630,23 +614,31 @@ def fragment_status(request: Request) -> HTMLResponse: "counts_sent": counts.get("sent", 0), "blocate_total": blocate_total, "blocate_defalcat": _blocate_defalcat(counts), - "blocate_actionabil": _blocate_actionabil(conn, account_id), + "pills_categorii": _pills_categorii(counts), "account_active": _account_active(conn, account_id), }) finally: conn.close() -def _is_iso_date(value: object) -> bool: - """True daca `value` e o data ISO YYYY-MM-DD (comparabila lexicografic corect).""" +def _iso_date_prefix(value: object) -> str | None: + """Intoarce primele 10 caractere (YYYY-MM-DD) daca incep cu o data ISO valida, altfel 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. + """ s = str(value or "").strip() - if len(s) != 10: - return False + if len(s) < 10: + return None + prefix = s[:10] try: - datetime.strptime(s, "%Y-%m-%d") - return True + datetime.strptime(prefix, "%Y-%m-%d") + return prefix except (ValueError, TypeError): - return False + return None # Stari care semnaleaza o problema ce necesita atentia operatorului. Eticheta umana @@ -693,6 +685,9 @@ def _submission_row_view(r) -> dict: } +_PAGE_SIZE = 25 # Marime pagina fixa (US-004 PRD 5.10) + + @router.get("/_fragments/submissions", response_class=HTMLResponse) def fragment_submissions( request: Request, @@ -700,12 +695,14 @@ def fragment_submissions( vehicul: str | None = None, data_de: str | None = None, data_pana: str | None = None, + page: int = 1, ) -> HTMLResponse: - """Tabel Trimiteri, scoped pe cont, cu filtre optionale (US-009). + """Tabel Trimiteri, scoped pe cont, cu filtre optionale si paginare (US-009, US-004). - Filtrarea pe stare se face in SQL (foloseste idx_submissions_account_status); - filtrarea pe vehicul (nr/VIN, case-insensitive) si pe interval data_prestatie - se face dupa parsarea payload_json in Python (plafon perf notat — eng review). + US-004 H1: 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). """ account_id = require_login(request) status = (status or "").strip() or None @@ -713,6 +710,9 @@ def fragment_submissions( data_de = (data_de or "").strip() or None data_pana = (data_pana or "").strip() or None filtru_activ = bool(status or vehicul_q or data_de or data_pana) + filtru_python = bool(vehicul_q or data_de or data_pana) # filtru care necesita Python + + page = max(1, page) # pre-clamp >= 1 conn = get_connection() try: @@ -722,45 +722,79 @@ def fragment_submissions( if status: where.append("status=?") params.append(status) - # Filtrarea pe vehicul/data se face in Python (dupa parsarea payload). Daca am - # taia la LIMIT inainte de filtru, am rata silentios randuri mai vechi care - # potrivesc. Cand un filtru text/data e activ, scoatem LIMIT-ul din SQL si plafonam - # afisarea dupa filtrare (OK la scara actuala — plafon perf notat, eng review). - limit_sql = "" if (vehicul_q or data_de or data_pana) else " LIMIT 200" - rows = conn.execute( - "SELECT id, status, id_prezentare, rar_status_code, rar_error, retry_count, " - "updated_at, payload_json FROM submissions " - f"WHERE {' AND '.join(where)} ORDER BY id DESC{limit_sql}", - params, - ).fetchall() + where_sql = " AND ".join(where) - view = [] - for r in rows: - v = _submission_row_view(r) - prez = v["prez"] - if vehicul_q: - hay = f"{prez['vehicul_nr']} {prez['vin']}".upper() - if vehicul_q not in hay: - continue - if data_de or data_pana: - d = prez["data_prestatie"] - # Comparam doar date in format ISO (YYYY-MM-DD); altfel comparatia de string - # ar fi gresita (ex. "05.12.2024"). Valori ne-ISO sunt excluse din filtru. - if not _is_iso_date(d): - continue - if data_de and d < data_de: - continue - if data_pana and d > data_pana: - continue - view.append(v) - if len(view) >= 200: - break + 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) + 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", + params, + ).fetchall() + + view_all: list[dict] = [] + for r in rows_db: + v = _submission_row_view(r) + prez = v["prez"] + if vehicul_q: + hay = f"{prez['vehicul_nr']} {prez['vin']}".upper() + if vehicul_q not in hay: + continue + if data_de or data_pana: + # Extragem portiunea YYYY-MM-DD (US-001 fix). + d_prefix = _iso_date_prefix(prez["data_prestatie"]) + if d_prefix is None: + continue + if data_de and d_prefix < data_de: + continue + if data_pana and d_prefix > data_pana: + continue + view_all.append(v) + + 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 + offset = (page - 1) * _PAGE_SIZE + view = view_all[offset:offset + _PAGE_SIZE] + + else: + # Calea A: SQL COUNT(*) + LIMIT/OFFSET (eficient, fara filtru Python activ) + total = conn.execute( + f"SELECT COUNT(*) FROM submissions WHERE {where_sql}", params + ).fetchone()[0] + + pages = max(1, math.ceil(total / _PAGE_SIZE)) if total > 0 else 1 + page = max(1, min(page, pages)) # clamp H2 + offset = (page - 1) * _PAGE_SIZE + + 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 " + "LIMIT ? OFFSET ?", + params + [_PAGE_SIZE, offset], + ).fetchall() + view = [_submission_row_view(r) for r in rows_db] + + page_start = (page - 1) * _PAGE_SIZE + 1 if total > 0 else 0 + page_end = min(page * _PAGE_SIZE, total) return templates.TemplateResponse("_submissions.html", { "request": request, "rows": view, "filtru_activ": filtru_activ, "csrf_token": get_csrf_token(request), + # Paginare (US-004) + "total": total, + "page": page, + "pages": pages, + "page_start": page_start, + "page_end": page_end, + # Filtre curente pentru linkurile de paginare (pastreaza filtrele, H2) + "f_status": status or "", + "f_vehicul": vehicul_q or "", + "f_data_de": data_de or "", + "f_data_pana": data_pana or "", }) finally: conn.close() @@ -768,6 +802,9 @@ def fragment_submissions( # Stari ne-trimise blocate pe care le putem corecta inline (US-010). _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). +_EDITABILE_OP = ("needs_data", "needs_mapping", "error") # Stari gestionabile prin lifecycle web (US-011): sterge / re-pune in coada. _GESTIONABILE_WEB = ("error", "needs_data", "needs_mapping") @@ -851,12 +888,31 @@ def _detaliu_ctx(request: Request, row, *, message: str | None = None, eticheta = eticheta_stare(row["status"]) nemapate_inline: list[dict] = [] nomenclator: list[dict] = [] + # Variabila interna: nomenclatorul complet (incarcat pentru needs_mapping, refolosit pt US-006) + _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. - nomenclator = load_nomenclator(conn) - nemapate_inline = _nemapate_pentru_submission(row, nomenclator) - if not nemapate_inline: - nomenclator = [] # nu expunem dropdown-ul cand nu exista operatii de mapat + _nomenclator_complet = load_nomenclator(conn) + 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_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 = "" + try: + _pd = json.loads(row["payload_json"] or "{}") + _prestatii = (_pd.get("prestatii") or []) if isinstance(_pd, dict) else [] + if _prestatii and isinstance(_prestatii[0], dict): + cod_prestatie_curent = (_prestatii[0].get("cod_prestatie") or "").strip().upper() + except (ValueError, TypeError): + pass + ctx = { "request": request, "csrf_token": get_csrf_token(request), @@ -882,6 +938,9 @@ def _detaliu_ctx(request: Request, row, *, message: str | None = None, # PRD 5.7: mapare inline (operatii nemapate ale acestui rand + nomenclator) "nemapate_inline": nemapate_inline, "nomenclator": nomenclator, + # US-006: select cod_prestatie pentru stari editabile + "nomenclator_rar": nomenclator_rar, + "cod_prestatie_curent": cod_prestatie_curent, "corectie_msg": message, "corectie_error": error, "corectie_errors": corectie_errors or [], @@ -1011,6 +1070,31 @@ 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"). + _cod_raw = form.get("cod_prestatie") + cod_prestatie_form = (_cod_raw.strip().upper() if isinstance(_cod_raw, str) else "") + if cod_prestatie_form: + exists_nom = conn.execute( + "SELECT 1 FROM nomenclator_rar WHERE cod_prestatie=?", (cod_prestatie_form,) + ).fetchone() + if not exists_nom: + return templates.TemplateResponse( + "_trimitere_detaliu.html", + _detaliu_ctx( + request, row, conn=conn, account_id=account_id, error=True, + message=f"Cod RAR necunoscut in nomenclator: {cod_prestatie_form}. " + "Alege un cod valid din lista.", + ), + ) + prestatii_form = content.get("prestatii") + if isinstance(prestatii_form, list) and prestatii_form: + p0 = dict(prestatii_form[0]) + p0["cod_prestatie"] = cod_prestatie_form + content["prestatii"] = [p0] + list(prestatii_form[1:]) + # Re-rezolva prestatiile cu maparea curenta (ca reresolve_account): NU re-pune # niciodata in coada un cod nemapat (codPrestatie null) — FINALIZATA e ireversibil # la RAR. Corectia campurilor de continut nu poate deebloca o operatie nemapata. @@ -1133,14 +1217,114 @@ async def post_corectie_trimitere(request: Request, submission_id: int) -> HTMLR 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. - Scoped pe sesiune (404 cross-account/inexistent, 409 sent/sending). Re-randeaza - panoul de detaliu cu starea noua + nudge `trimiteriChanged` pentru lista. + 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. """ 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 + # 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 "") + + if cod_prestatie_form: + row = _fetch_submission_scoped(conn, account_id, submission_id) + if not row: + raise HTTPException(status_code=404, detail="trimitere inexistenta") + if row["status"] != "error": + # cod_prestatie acceptat DOAR pentru starea error prin /repune + raise HTTPException( + status_code=409, + detail="modificarea cod_prestatie prin repune e valida doar pentru starea error", + ) + # Valideaza cod-ul fata de nomenclator + exists_nom = conn.execute( + "SELECT 1 FROM nomenclator_rar WHERE cod_prestatie=?", (cod_prestatie_form,) + ).fetchone() + if not exists_nom: + return templates.TemplateResponse( + "_trimitere_detaliu.html", + _detaliu_ctx( + request, row, conn=conn, account_id=account_id, + error=True, + message=f"Cod RAR necunoscut: {cod_prestatie_form}. Alege un cod valid.", + ), + ) + # Parseaza payload si injecteaza cod_prestatie + try: + content = json.loads(row["payload_json"]) if row["payload_json"] else {} + if not isinstance(content, dict): + content = {} + except (ValueError, TypeError): + content = {} + prestatii = content.get("prestatii") or [] + if isinstance(prestatii, list) and prestatii: + p0 = dict(prestatii[0]) + p0["cod_prestatie"] = cod_prestatie_form + # sterge cod_op_service/denumire daca exista (codul direct preia prioritate) + p0.pop("cod_op_service", None) + content["prestatii"] = [p0] + list(prestatii[1:]) + # Re-rezolva prestatii cu noul cod + mapping_meta = load_mapping_meta(conn, account_id) + mapping = {op: m["cod_prestatie"] for op, m in mapping_meta.items()} + valid_codes = load_nomenclator_codes(conn) or None + text_rules = load_text_rules(conn, account_id) + resolved, _unmapped = resolve_prestatii(content.get("prestatii"), mapping, valid_codes, text_rules) + content["prestatii"] = resolved + # Canonicalize + rebuild idempotency key + canon = canonicalize_row(content) + payload_json = json.dumps(content, ensure_ascii=False) + new_key = build_key(account_id, canon) + # Verifica coliziune (numai daca cheia s-a schimbat) + if new_key != row["idempotency_key"]: + dup = conn.execute( + "SELECT id FROM submissions WHERE idempotency_key=? AND id<>?", + (new_key, row["id"]), + ).fetchone() + if dup: + return templates.TemplateResponse( + "_trimitere_detaliu.html", + _detaliu_ctx( + request, row, conn=conn, account_id=account_id, + error=True, + message=f"Exista deja o trimitere identica (rand #{dup['id']}).", + ), + ) + try: + conn.execute( + "UPDATE submissions SET idempotency_key=?, status='queued', payload_json=?, " + "rar_error=NULL, retry_count=0, next_attempt_at=datetime('now'), " + "updated_at=datetime('now') WHERE id=? AND account_id=?", + (new_key, payload_json, row["id"], account_id), + ) + conn.commit() + except sqlite3.IntegrityError: + return templates.TemplateResponse( + "_trimitere_detaliu.html", + _detaliu_ctx( + request, row, conn=conn, account_id=account_id, + error=True, + message="Exista deja o trimitere identica. Operatia a fost oprita.", + ), + ) + row2 = _fetch_submission_scoped(conn, account_id, submission_id) + resp = templates.TemplateResponse( + "_trimitere_detaliu.html", + _detaliu_ctx( + request, row2, conn=conn, account_id=account_id, + message="Cod actualizat — randul a fost re-pus in coada.", + ), + ) + resp.headers["HX-Trigger"] = "trimiteriChanged" + return resp + + # Cale normala: fara cod_prestatie → delega la requeue_submission try: requeue_submission(conn, account_id, submission_id) except SubmissionNotFound: diff --git a/app/web/static/fonts/IBMPlexMono-Regular-latin-ext.woff2 b/app/web/static/fonts/IBMPlexMono-Regular-latin-ext.woff2 new file mode 100644 index 0000000..53fb6e5 Binary files /dev/null and b/app/web/static/fonts/IBMPlexMono-Regular-latin-ext.woff2 differ diff --git a/app/web/static/fonts/IBMPlexMono-Regular-latin.woff2 b/app/web/static/fonts/IBMPlexMono-Regular-latin.woff2 new file mode 100644 index 0000000..a6c77d6 Binary files /dev/null and b/app/web/static/fonts/IBMPlexMono-Regular-latin.woff2 differ diff --git a/app/web/static/fonts/IBMPlexSans-Bold-latin-ext.woff2 b/app/web/static/fonts/IBMPlexSans-Bold-latin-ext.woff2 new file mode 100644 index 0000000..b6c0c44 Binary files /dev/null and b/app/web/static/fonts/IBMPlexSans-Bold-latin-ext.woff2 differ diff --git a/app/web/static/fonts/IBMPlexSans-Bold-latin.woff2 b/app/web/static/fonts/IBMPlexSans-Bold-latin.woff2 new file mode 100644 index 0000000..da7d57f Binary files /dev/null and b/app/web/static/fonts/IBMPlexSans-Bold-latin.woff2 differ diff --git a/app/web/static/fonts/IBMPlexSans-Medium-latin-ext.woff2 b/app/web/static/fonts/IBMPlexSans-Medium-latin-ext.woff2 new file mode 100644 index 0000000..d24a8a3 Binary files /dev/null and b/app/web/static/fonts/IBMPlexSans-Medium-latin-ext.woff2 differ diff --git a/app/web/static/fonts/IBMPlexSans-Medium-latin.woff2 b/app/web/static/fonts/IBMPlexSans-Medium-latin.woff2 new file mode 100644 index 0000000..adbbd4c Binary files /dev/null and b/app/web/static/fonts/IBMPlexSans-Medium-latin.woff2 differ diff --git a/app/web/static/fonts/IBMPlexSans-Regular-latin-ext.woff2 b/app/web/static/fonts/IBMPlexSans-Regular-latin-ext.woff2 new file mode 100644 index 0000000..24f5f3a Binary files /dev/null and b/app/web/static/fonts/IBMPlexSans-Regular-latin-ext.woff2 differ diff --git a/app/web/static/fonts/IBMPlexSans-Regular-latin.woff2 b/app/web/static/fonts/IBMPlexSans-Regular-latin.woff2 new file mode 100644 index 0000000..93bcd64 Binary files /dev/null and b/app/web/static/fonts/IBMPlexSans-Regular-latin.woff2 differ diff --git a/app/web/static/romfast_logo.png b/app/web/static/romfast_logo.png new file mode 100644 index 0000000..23a0e33 Binary files /dev/null and b/app/web/static/romfast_logo.png differ diff --git a/app/web/templates/_coada.html b/app/web/templates/_coada.html index c2440e3..6cec9fa 100644 --- a/app/web/templates/_coada.html +++ b/app/web/templates/_coada.html @@ -28,21 +28,13 @@ hx-swap="innerHTML" hx-trigger="submit, change, keyup delay:400ms from:input[name='vehicul']" style="display:flex; gap:10px; flex-wrap:wrap; align-items:flex-end; margin-bottom:12px;"> -
- - {# US-014/T13: status_filtru (din deep-link ?tab=acasa&status=) pre-selecteaza - starea, iar submissions-wrap (hx-include #filtre-trimiteri) o incarca filtrat. #} - {% set sf = status_filtru | default('') %} - -
+ {# US-003 (PRD 5.10): dropdown status eliminat — inlocuit cu pill-uri in bara de status. + Filtrul de stare vine de la pill-uri (/_fragments/submissions?status=X direct). + Camp hidden permite reset stare la submit manual din form (Filtreaza). #} + + {# US-004 (PRD 5.10): pagina curenta — actualizata prin OOB swap din _submissions.html. + Poll-ul (hx-include="#filtre-trimiteri") include automat pagina curenta (L2 PRD). #} +
diff --git a/app/web/templates/_mapari.html b/app/web/templates/_mapari.html index 31c02ac..0ada958 100644 --- a/app/web/templates/_mapari.html +++ b/app/web/templates/_mapari.html @@ -21,26 +21,11 @@ {# US-005 (5.5): antet standard + link Ajutor ca
nativ (fara JS). Toata proza care inainte se repeta inline (scopul maparilor, Auto/Manual) traieste acum AICI, o singura data, ascunsa implicit. #} + {# US-010: sectiunea de ajutor (details.ajutor-mapari) eliminata. + Empty-state „Nicio operatie nemapata" eliminat — sectiunea ramane goala (fara text). #}

De rezolvat

-
- Ajutor -
- Maparile leaga o operatie din softul tau (cod intern ROAAUTO) de un cod RAR oficial. - Operatiile necunoscute raman blocate in needs_mapping - si NU pleaca la RAR pana le mapezi. Sugestiile (%) vin din potrivire fuzzy pe denumire — - verifica-le inainte sa salvezi. In coada: Auto = la - urmatoarele fisiere cu aceasta operatie randurile intra automat in coada; - Manual = raman pentru verificare, nimic nu pleaca la RAR pana confirmi. - La schimbarea unui cod, submission-urile blocate pe acea operatie se re-rezolva automat. -
-
- {% if not pending %} -
- Nicio operatie nemapata — tot ce a venit s-a tradus in coduri RAR. - Importa un fisier nou daca vrei sa adaugi prezentari. -
- {% else %} + {% if pending %}
- {# Salveaza/Sterge in meniu contextual (kebab) — randul ramane ingust. Butoanele se - leaga prin form= de cele doua form-uri hx-post definite in prima celula a randului. #} -
- -
- - -
-
+ {# US-011: butoane icon mereu vizibile (fara kebab). SVG aria-hidden; aria-label pe buton. + data-dirty-form e citit de JS din base.html: la schimbarea select-ului din acelasi rand, + JS adauga clasa "dirty" pe butonul de salvare (fundal --accent = modificari nesalvate). #} + + {% endfor %} @@ -187,79 +181,8 @@
- - -
-

Formate de coloane salvate

- - {% if not column_formats %} -
- Niciun format de coloane salvat inca. La primul import, maparea coloanelor fisierului - se retine aici si se reaplica automat la fisierele cu acelasi antet. -
- {% else %} -

- Antetele de fisier recunoscute. Un fisier cu alte coloane = format nou separat. -

- -
-
- -
-
- - - - - - - - - {% for f in column_formats %} - - - - - - - {% endfor %} - -
ColoaneMapari (coloana → camp)Format data
- {{ f.columns | length }} coloane - - {% for col, camp in f.mappings.items() %} - {{ col }} → {{ camp }}{% if not loop.last %}; {% endif %} - {% endfor %} - -
- - - - -
-
-
- - - -
-
-
- -
-
- {% endif %} -
- - - + +

Reguli automate (text)

@@ -354,4 +277,77 @@
+ + + + +
+

Formate de coloane salvate

+ + {% if not column_formats %} +
+ Niciun format de coloane salvat inca. La primul import, maparea coloanelor fisierului + se retine aici si se reaplica automat la fisierele cu acelasi antet. +
+ {% else %} +

+ Antetele de fisier recunoscute. Un fisier cu alte coloane = format nou separat. +

+ +
+
+ +
+
+ + + + + + + + + {% for f in column_formats %} + + + + + + + {% endfor %} + +
ColoaneMapari (coloana → camp)Format data
+ {{ f.columns | length }} coloane + + {% for col, camp in f.mappings.items() %} + {{ col }} → {{ camp }}{% if not loop.last %}; {% endif %} + {% endfor %} + +
+ + + + +
+
+
+ + + +
+
+
+ +
+
+ {% endif %} +
+
diff --git a/app/web/templates/_status.html b/app/web/templates/_status.html index 35749a9..7249d09 100644 --- a/app/web/templates/_status.html +++ b/app/web/templates/_status.html @@ -1,3 +1,15 @@ +
- - {% if blocate_actionabil %} + + {% if pills_categorii %}
-
Necesita atentia ta
-
- {% for cat in blocate_actionabil %} -
- {# Link: filtreaza lista Trimiteri pe aceasta stare (HTMX in-place) cu fallback - deep-link server-side (?tab=acasa&status=...). #} - - {{ cat.eticheta[0] }} ({{ cat.n }}) › - -
    - {% for r in cat.randuri %} -
  • - #{{ r.id }} {{ r.vin }}{% if r.nr %} / {{ r.nr }}{% endif %} -
  • - {% endfor %} - {% if cat.rest %} -
  • …si inca {{ cat.rest }}
  • - {% endif %} -
-
+
+ Necesita atentie: + {% for pill in pills_categorii %} + {% endfor %} + {# Buton "Toate" — reseteaza filtrul de categorie #} +
{% endif %} diff --git a/app/web/templates/_submissions.html b/app/web/templates/_submissions.html index bf54f4e..0744ac8 100644 --- a/app/web/templates/_submissions.html +++ b/app/web/templates/_submissions.html @@ -1,3 +1,10 @@ +{# + OOB: actualizeaza inputul id="f-page" din #filtre-trimiteri (US-004 L2). + Poll-ul de 15s (hx-include="#filtre-trimiteri") preia automat pagina curenta. + Elementul OOB e extras din continutul normal de HTMX inainte de swap in #submissions-wrap. +#} + + {% if rows %} {# US-011: form de stergere bulk. Selectia opereaza DOAR pe randuri blocate (gestionabil); sent/sending/queued nu au checkbox (read-only). #} @@ -61,7 +68,8 @@ {{ r.prez.vehicul_nr }} {% if r.prez.vin_scurt and r.prez.vin_scurt != '—' %} - {{ r.prez.vin_scurt }} + {# US-005: VIN pe rand separat sub nr (element block, nu span inline) #} +
{{ r.prez.vin_scurt }}
{% endif %} @@ -83,6 +91,105 @@
+ +{# + Paginare numerotata (US-004 PRD 5.10). + Afisata doar cand exista mai mult de o pagina. + Fiecare link pastreaza filtrele curente (status, vehicul, data_de, data_pana). + Pagina curenta: aria-current="page" (semantic). +#} +{% if total is defined %} +
+ {% if total == 0 %} + 0 trimiteri + {% else %} + {{ page_start }}–{{ page_end }} din {{ total }} + {% endif %} +
+{% endif %} + +{% if pages is defined and pages > 1 %} +{# + Construim param-string pentru filtrele curente (fara page) — refolosit in fiecare link. + Filtrul status vine din pill-uri (nu din form); il pastram in URL. +#} +{% set pq = "" %} +{% if f_status %}{% set pq = pq + "&status=" + f_status %}{% endif %} +{% if f_vehicul %}{% set pq = pq + "&vehicul=" + f_vehicul %}{% endif %} +{% if f_data_de %}{% set pq = pq + "&data_de=" + f_data_de %}{% endif %} +{% if f_data_pana %}{% set pq = pq + "&data_pana=" + f_data_pana %}{% endif %} + + +{% endif %} + {% elif filtru_activ %}
Nimic pe filtrul curent. diff --git a/app/web/templates/_trimitere_detaliu.html b/app/web/templates/_trimitere_detaliu.html index aafbc81..3fb31c1 100644 --- a/app/web/templates/_trimitere_detaliu.html +++ b/app/web/templates/_trimitere_detaliu.html @@ -19,8 +19,10 @@

{{ stare_subtext }}

{% endif %} - {# === R10 (2): bloc eroare blocanta cand exista === #} - {% if erori_3n %} + {# === R10 (2): bloc eroare blocanta — DOAR in read-only (US-008). + In editare, cardul 3-niveluri e inlocuit cu: erori per-camp in macro `camp` + (text simplu .s-error) + rezumat top-of-form pentru erori fara camp (mai jos). === #} + {% if not editabil and erori_3n %}
{{ card_erori(erori_3n) }}
@@ -88,6 +90,13 @@ {% if corectie_error %}role="alert"{% endif %}>{{ corectie_msg }}
{% endif %} + {# US-008 (M6): erori fara camp (field None) nu dispar silentios in editare — + cardul 3n e ascuns, deci adaugam un rezumat simplu top-of-form. + Erori cu camp raman afisate per-camp de macro-ul `camp` de mai jos. #} + {% for e in erori_3n if not e.field %} + + {% endfor %} + {% macro camp(nume, eticheta, valoare, tip='text') %}
@@ -105,11 +114,40 @@ hx-disabled-elt="find button"> + {# US-006: select cod RAR pe stari editabile (needs_data/needs_mapping), cu nomenclator. + Read-only pe sent/sending/queued/error (nomenclator_rar gol → ramura else). #} + {% if nomenclator_rar %} +
+ + {% if prez.operatie and prez.operatie != '—' %} +
{{ prez.operatie }}
+ {% endif %} + +
+ {% else %} {# Operatie + cod RAR read-only deasupra campurilor (R9, fara eticheta „Cod RAR"). #}
Operatie
{{ prez.operatie }} · {{ cod_afis }}
+ {% endif %} + + {# US-007: operatie service (cod intern + denumire venita prin API/import), distinct de + operatia RAR mapata. Conventie US-002: op_service_cod="" cand lipseste → randul absent. #} + {% if prez.op_service_cod %} +
+
Operatie service
+
{{ prez.op_service_cod }}{% if prez.op_service_denumire %} — {{ prez.op_service_denumire }}{% endif %}
+
+ {% endif %} {# Nr. inmatriculare pe rand propriu, VIN dedesubt — ambele latime plina. #} {{ camp('nr_inmatriculare', 'Numar inmatriculare', form_nr) }} @@ -139,6 +177,12 @@
{{ prez.vin }}
Operatie
{{ prez.operatie }} · {{ cod_afis }}
+ {# US-007: operatie service (cod intern + denumire), distinct de operatia RAR. + Conventie US-002: op_service_cod="" cand lipseste → randul absent (fara "—"). #} + {% if prez.op_service_cod %} +
Operatie service
+
{{ prez.op_service_cod }}{% if prez.op_service_denumire %} — {{ prez.op_service_denumire }}{% endif %}
+ {% endif %}
Data prestatie
{{ prez.data_prestatie }}
Odometru final
{{ prez.odometru }}
@@ -147,12 +191,30 @@ {# === R10 (5): actiuni de jos — primar Re-pune (doar error) + Sterge pe RAND SEPARAT (R2/R11) === #} {% if status == 'error' or gestionabil %}
- {# R2: error -> buton primar „Re-pune in coada" pe /repune (error nu e editabil). #} + {# R2: error -> buton primar „Re-pune in coada" pe /repune (error nu e editabil pentru #} + {# campuri vehicul, dar US-006b permite schimbarea cod_prestatie prin acelasi formular). #} {% if status == 'error' %}
+ {# US-006b: select cod_prestatie optional in formularul /repune (doar pentru error). #} + {% if nomenclator_rar %} + + + {% endif %}
{% endif %} diff --git a/app/web/templates/base.html b/app/web/templates/base.html index 81ec918..131809c 100644 --- a/app/web/templates/base.html +++ b/app/web/templates/base.html @@ -14,12 +14,16 @@ htmx.config.useTemplateFragments = true; + {# US-012 (PRD 5.10): grila 3 coloane — stanga (env badge) | centru (titlu+wordmark) | dreapta (controale). #}
-

Gateway RAR AUTOPASS

- {{ rar_env }} -
+ {# Celula stanga: badge env (test/prod) — echilibru optic fata de controalele din dreapta #} +
+ {{ rar_env }} +
+ {# Celula centru: titlu + wordmark 'by ROMFAST' #} +
+

Gateway RAR AUTOPASS

+ {# US-012b (decizie user): logo PNG real in loc de wordmark text. + 288x175 RGBA fundal transparent — lizibil pe light/dark/petrol fara filtre. #} + +
+ {# Celula dreapta: comutator tema + versiune + meniu cont #} +
@@ -331,6 +439,10 @@ aria-haspopup="true" aria-expanded="false" aria-controls="cont-menu" aria-label="Meniu cont" title="Meniu cont">☰
+ {# aria-live pentru anuntarea schimbarilor de tema (US-014, accesibilitate) #} +
{% block content %}{% endblock %}
{# Modal detaliu trimitere (PRD 5.9 US-003): container global, SIBLING al
(nu descendent), ca `inert`+`aria-hidden` pe
sa nu-l prinda si pe el (R7). @@ -360,36 +475,46 @@
@@ -470,6 +595,19 @@ window.addEventListener('resize', function() { closeAll(null); }); })(); + - {% endblock %} diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 492d031..62c6ad2 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -48,7 +48,7 @@ Reguli de contract (detalii in `docs/api-rar-contract.md`): `FINALIZATA` e termi > PRD-uri (`docs/prd/prd-X.Y-*.md`), linkate in coloana Detalii. La fiecare livrabila terminata: > schimba statusul + data + linkul PRD si actualizeaza "Ultima actualizare". -**Ultima actualizare**: 2026-06-24 — 5.8 LIVRAT (UX tabel trimiteri + reguli mapare pe text). 11 stories/4 valuri, TDD prin echipa de workeri (lead orchestreaza, nu scrie cod). Reguli text per cont (`operation_text_rules`, substring insensibil diacritice/caz): `resolve_prestatii` capata param aditiv `text_rules` cu precedenta stricta (cod direct > mapare exacta > regula text > nemapat), threadat pe TOATE cele 6 callsite-uri + `valid_codes` + seam `classify_prezentare` (dry-run = trimitere reala); UI Mapari sectiune noua + preview pre-salvare + avertizare overlap + telemetrie `text_rule_hit` in jurnal. UX tabel: `cod_rar` sub operatie, `eticheta_scurta` pill, fara scroll orizontal (scopat `.tabel-trimiteri` + carduri <768px), detaliu INLINE rand-sibling expandabil (chevron+fundal+a11y, pauza poll 15s). VERIFY context curat: **814 passed, 1 skipped** (live opt-in); E2E functional TestClient (browser pixel-level + live RAR neprobate — sandbox/login/lipsa creds). **`/code-review high`: 1 bug critic reparat** — regula text `auto_send=0` (DEFAULT, decizia CEO siguranta) trimitea automat la RAR in loc sa TINA randul pentru review (`has_no_auto_send` ignora regula text care a rezolvat itemul); reparat TDD + curatare adnotari stale (repara si o telemetrie falsa latenta). Backend trimitere + schema-send NEATINSE (schema pur aditiv). PRD: [prd-5.8](prd/prd-5.8-ux-tabel-trimiteri-reguli-text.md). | 2026-06-24 — 5.8 PLAN: eng + CEO review rulate (6 decizii incorporate, 11 stories/4 valuri). | 2026-06-23 — 5.7 LIVRAT (raspuns API onest la blocaje + mapare inline din detaliu). Raportat din client VFP: `POST /v1/prezentari` intorcea `submission_id`+`status` FARA motiv pe randuri blocate (`erori` se popula doar pe ramura `on_unmapped_error=True`) → un `needs_data`/`needs_mapping` parea succes ("raspuns fara erori"). Reparat ADITIV: `SubmissionResult` += `nemapate` + `motiv`; `create_prezentari` populeaza `erori` (validare continut, 3 niveluri) / `nemapate` (coduri fara mapare, COD_NEMAPAT) / `motiv` (rezumat uman) pe TOATE caile non-`queued` — enqueue, respins (`on_unmapped_error=True`) si reactivare dedup peste `error`. UI: mapare inline in panoul de detaliu trimitere (`POST /trimitere/{id}/mapeaza`, reuse EXACT `save_mapping`+`reresolve_account`, scoped sesiune + CSRF, re-rezolva pe `batch_id`-ul randului → deblocheaza si randurile-frate; apare doar pe operatii nemapate reale, nu pe auto_send=0) — fara drum prin tab-ul Mapari. `/code-review high`: 2 buguri reale reparate (reactivarea omitea `erori`/`nemapate`/`motiv`; dublu `load_nomenclator` in `_detaliu_ctx`), restul candidatilor infirmati la verify (parse `auto_send` corect via `or ""`; lipsa `conn/account_id` pe ramuri de corectie nereachable cu needs_mapping+unmapped). `pytest -q` **765 passed, 0 failed** (+1 skipped live). **PROBA LIVE `--send` (2026-06-23): mapare inline E2E pe RAR test** — `POST /v1/prezentari` cu operatie nemapata → `needs_mapping` (raspuns onest cu `nemapate`+`motiv`) → mapare inline din panoul de detaliu in browser (Playwright) → `queued` → worker login RAR + `postPrezentare` → `sent idPrezentare=68827`, confirmat independent in finalizate RAR + jurnal `app_events` (`rar_login ok` → `submission_sent`). Automatizat ca test live opt-in `tests/test_live_rar.py` (skip implicit; `AUTOPASS_LIVE_RAR=1` + creds test reproduc tot lantul → `idPrezentare=68828`). PRD: [prd-5.7](prd/prd-5.7-raspuns-onest-mapare-inline.md). | ISTORIC: FIX out-of-process (raportat din client VFP): `cod_prestatie` necunoscut in nomenclator era trimis raw la RAR → **HTTP 500** (`ORA-12899`, coloana `COD_PRESTATIE` max 5 car.) + record PARTIAL `FINALIZATA` (RAR ne-tranzactional) pe care reconcilierea il marca fals `sent`. Reparat: validare `cod_prestatie` fata de nomenclator la ingestie (cod necunoscut → tratat ca operatie de mapat, nu se mai trimite raw) + optiune boolean `on_unmapped_error` (`false` default → needs_mapping | `true` → respinge) per-cerere cu default per-cont `accounts.on_unmapped_error_default` (migrare aditiva). Confirmat live raspunsul RAR (500 pe cod intern vs 200 pe `OE-1`). Inclus si in `c842e33`: fix lease orfan worker (nepotrivire format data sending_since vs cutoff → orice rand `sending` parea expirat) + guard anti-dublu-POST + fix UI `hx-confirm` mostenit pe randuri (alerta de stergere la click pe rand). Teste: **748 passed** (cele 2 esecuri pre-existente fara legatura). Contract + CLAUDE.md actualizate. | 5.6 IMPLEMENTAT + VERIFY PASS (asteapta commit). Cele 14 stories din PRD 5.6 livrate TDD (RED->GREEN), `pytest -q` **741 passed, 0 failed**. Lifecycle trimiteri blocate (Val A primul, decizie #18): `app/submissions_admin.py` (sterge/repune scoped, 404-before-409); reactivare dedup peste `error` cu CAS + invalidare sesiune worker la creds noi (T1) + propagare `accounts.rar_creds_enc` (#17) + camp aditiv `reactivated:true` (#19); retentie randuri blocate 30z + `purge_after` curatat la reactivare/requeue (T2); API `DELETE`/`/repune` (200+JSON, #20); UI butoane + bulk + banner "Necesita atentia ta" actionabil cu deep-link. Observabilitate: `app/observ.py log_event` (dublu canal `app_events` DB + `RotatingFileHandler` per-proces, redactare creds/PII la scriere via `app/security.redact_pii`/`vin_partial`), `request_id` middleware + `X-Request-ID` pe toate raspunsurile (T8), handler global excepții -> 500 envelope 6-chei + request_id (T7), audit cerere API (`api_prezentari`/`api_auth_esuat`) + audit worker (`rar_login`/tranzitii), tab "Jurnal" filtrabil scoped (non-admin doar contul sau), retentie jurnal 90z. Live RAR `--send` NEPROBAT in sesiune (recomandat la deploy: confirma `rar_login` ok + `submission_sent` in jurnal). PRD actualizat cu raport VERIFY; contract actualizat cu endpointurile noi (T10). | ISTORIC: HOTFIX livrat + 5.6 APROBAT. Hotfix 500 pe `POST /v1/prezentari` (raportat din client Visual FoxPro): `AUTOPASS_CREDS_KEY` din `.env` nu respecta formatul Fernet (32 bytes url-safe base64) → `ValueError` la primul `encrypt_creds` → 500 brut. Reparat: cheie Fernet valida in `.env` + `crypto.validate_creds_key()` apelata in `main.lifespan` (fail-fast la startup, mesaj clar in loc de 500 la primul POST). Confirmat live: POST VFP → 200 `queued`; trimitere reala pe RAR test → `sent idPrezentare=68818` (verificat independent in finalizate). Corectat si mesajul fals din dashboard pentru starea `error` in `labels.py` ("se reincearca automat" → starea e terminala, NU se reincearca). Investigatia a expus 3 goluri structurale (500 brut fara traducere 3 niveluri; lipsa jurnal de aplicatie la nivel de eveniment; lacune de lifecycle — randuri blocate permanente, dedup blocat de un rand `error`, banner "Necesita atentia ta" neactionabil) → **PRD 5.6 APROBAT** (14 stories; decizii §5 rezolvate cu user). PRD: [prd-5.6](prd/prd-5.6-observabilitate-jurnal.md). | ISTORIC: 5.5 LIVRAT (uniformizare/standardizare UI/UX: tabele la grila Trimiteri, meniu hamburger + tab-bar redus Acasa/Mapari, sterge Ajutor de pe Acasa, panou admin cu selectie+bulk pe model nou `accounts.status`. 9 stories in 3 valuri, UI pur cu o singura exceptie backend = stare cont; stergere soft cu purjare PII imediata GDPR. VERIFY 671 teste + E2E browser (2 bug-uri prinse) + `/code-review high` (2 bug-uri reale reparate). Commit `1fbd894`, vezi randul 5.5). | ISTORIC: 5.4 LIVRAT (Erori pe 3 niveluri problema+cauza+fix pe API si UI: catalog central pur `app/errors.py` ca SINGURA sursa de adevar cod→{problema,fix}, consumat de API+UI+worker — face imposibila divergenta intre canale, acelasi invariant ca 5.2. 8 stories in 5 valuri. Tot ADITIV: `field`/`message`/`error` pastrate la octet, adaugam `cod/problema/cauza/fix`; `rar_error` stocat = SUPERSET (chei vechi intacte → `labels.py` nu se rupe intre valuri, zero migrare). Scope = fluxul de declarare; login/signup/CSRF neatinse. UI progresiv: lista compacta, 3 niveluri complete in detaliu/preview, AA light+dark. VERIFY context curat PASS 628 teste (byte-compat+superset verificate direct, E2E API+web; live RAR neprobat — lipsa creds key). `/code-review high`: 2 bug-uri reale reparate in `labels.py` (`motiv_uman` fara ramura 3-niveluri → 401 creds garbled in coloana Motiv; `parse_erori` element gol pe `{}`). 631 teste. Backend trimitere + schema NEATINSE. PRD: [prd-5.4](prd/prd-5.4-erori-3-niveluri.md)). | ISTORIC: 5.3 LIVRAT (Light/Dark mode: tema light ca bloc `[data-theme="light"]` peste variabilele `:root` — dark NESCHIMBAT la octet; comutator soare/luna in header pe toate paginile, default OS-aware cu fallback dark, persistenta `localStorage` doar la comutare explicita, script anti-FOUC in `` pre-paint; suprafetele de stare hardcodate convertite la `color-mix` in `base.html` + 7 fragmente. Zero backend — pur frontend. VERIFY 2 runde: r1 FAIL a prins literalii dark ramasi in 7 fragmente HTMX (text invizibil in light, test vacuu pe doar base.html) → fix US-003 + test care scaneaza fragmentele; r2 PASS E2E browser (banner light ~13:1 contrast, toggle instant+persista+anti-FOUC, dark identic). `/code-review` high: 1 finding reparat (light `--ok` green sub AA ca text → green-700, ~5.0:1). 584 teste. PRD: [prd-5.3](prd/prd-5.3-light-dark-mode.md)). | ISTORIC: 5.2 LIVRAT (Endpoint dry-run `POST /v1/prezentari/valideaza`: valideaza payload + mapare si intoarce verdictul real — `status_estimat` queued/needs_data/needs_mapping + erori `[{field,message}]` + coduri nemapate + prestatii rezolvate — FARA enqueue, FARA creds, zero scriere DB). 1 story TDD. Cheia de design: helper pur partajat `classify_prezentare` folosit de AMBELE rute, ca dry-run-ul sa nu poata diverge de trimiterea reala (invariant de corectitudine); `create_prezentari` refactorizat pe el cu comportament identic. Scope minim per decizie user: doar validare+mapare (fara idempotency/duplicat, `idempotency.py` neatins), hub `/integrare` amanat ca follow-up (descoperibilitate). VERIFY context curat PASS (577 teste; E2E API cu cele 3 verdicte + COUNT(*)=0 dupa dry-run + fara leak creds in raspuns; regresia de aur verde; live RAR `FINALIZATA` neprobat — lipsa creds key, endpoint read-only nu atinge worker/coada/schema). `/code-review` high: 0 findings (refactor faithful, mutable-default Pydantic-safe, import local necesar anti-circular). PRD: [prd-5.2](prd/prd-5.2-dryrun-valideaza.md). | ISTORIC: 5.1 LIVRAT (Hub de integrare `/integrare`: exemple cod multi-limbaj + retetar VFP cu 2 dialecte + `GET /v1/ping` readiness + export Postman/OpenAPI + "Testeaza conexiunea"). 4 stories in 2 valuri (Val 1 = US-001/US-002/US-004 paralel pe fisiere disjuncte via Agent team; Val 2 = US-003 UI). Atentie operationala: US-003 a rulat intr-un worktree branched din ultimul commit (FARA modificarile necomise ale US-004 din working-tree) si la "copiere manuala" a SUPRASCRIS `routes.py`, stergand ruta `POST /integrare/test-cheie` (8 teste 404) — reparat prin re-aplicarea rutei de catre autorul US-004 pe `routes.py` curent. Lectie: stories care ating acelasi fisier in valuri diferite + worktree = clobber daca worktree-ul nu vede working-tree-ul; foloseste fisiere disjuncte SAU merge atent de catre lead. VERIFY context curat PASS (568 teste) + E2E browser Playwright (deep-link server-side, IA pe 2 niveluri, VFP cu 3 niveluri de tab comuta corect, copy, htmx test-cheie → fragment eroare, 0 erori consola) + enqueue live (`POST /v1/prezentari` → queued); live RAR `FINALIZATA` NEPROBAT in sesiune (lipsa `AUTOPASS_CREDS_KEY`/creds RAR test) — risc minim, backend trimitere NEATINS. `/code-review` high a prins 4 bug-uri reale (toate in suprafata noua, reparate + lock-uite cu teste): snippet C# JSON multi-linie nevalid (CS1010), snippet VFP `json.dumps(indent=0)` inca cu newline-uri → string literal rupt in ambele dialecte, snippet Node `node:buffer` nu exporta FormData → TypeError, script `_integrare.html` ne-scoped acumuland event-listeneri pe tab-bar-ul principal la fiecare swap htmx (scoped pe `#integrare-section`). Notat ca cleanup viitor (nereparat): `_render_integrare` dubleaza SQL `are_creds`/`are_cheie`, `ping` cu 2 conexiuni DB + `account_for_key` de 2 ori, `_campuri_obligatorii` necache-uit, panouri limbaj copy-paste (candidat macro Jinja2). Backend trimitere (worker/masina stari/idempotenta/mapping) si schema NEATINSE. PRD: [prd-5.1](prd/prd-5.1-hub-integrare.md). | ISTORIC: 3.6 INCHIS (editare celule in preview + Acasa unificata). CLOSE: `/code-review` high a prins 1 bug real (decriptare `override_json` neprotejata de try/except in ambele cai de preview — 500 pe tot batch-ul la rotatie cheie Fernet vs. `raw_json` care degrada gratios), reparat in `import_router.preview_import` + `routes._web_compute_preview`; duplicarea `_override_of`/canonicalize notata ca cleanup viitor. 523 teste pass. 7 stories in 3 valuri, executate de 2 echipe in paralel (TeamCreate) pe fisiere disjuncte (core: routes/import/templates; mapari: `_mapari.html`) + US-007 secvential. Livrate: tab "Trimiteri" eliminat→sectiune "Trimiterile tale" sub upload pe Acasa (US-003); upload bara slim accentuata cu hero la first-run (US-004); editare de celule in preview prin `import_rows.override_json` (Approach B, Fernet, patch canonic aplicat ULTIMUL in `_resolve_row_for_preview`+`commit_import` — completeaza inclusiv coloane ABSENTE din fisier), mutatie pura cu status rederivat (US-001); buton Editeaza pe rand cu swap pe ``+OOB contoare (nu pe sectiune), form propriu, mutual-exclusion, reuse grila `_trimitere_detaliu.html` (US-002); Mapari + formate de coloane ca tabele `.tablewrap`, H4 auto_send stocat (US-005/006); bifa "auto-send"→comutator etichetat pe COADA ("Pune automat in coada"/"Tine pentru verificare"), scoped pe operatie, `name=auto_send` pastrat (zero backend) (US-007). 523 teste pass. **VERIFY**: E2E browser pe `/` (Acasa unificata, upload slim, editare rand needs_data→ok cu swap pe rand + contoare OOB, Mapari tabelar + comutator) + LIVE pe RAR test — import fara coloana data → editarea completeaza data (override) → commit → worker login RAR test → `postPrezentare` → `sent` cu `idPrezentare=68696`, confirmat independent in lista finalizate RAR. 3 bug-uri JS (htmx 1.9.12) prinse DOAR la E2E in browser (invizibile la TestClient) si reparate: `useTemplateFragments=true` (raspunsul ``+OOB era parsat in context de tabel → `swapError` + contoare pierdute), re-activare `confirm-btn` deferita pe tick (race `editing=true` tranzitoriu), `n-hint` ok-count actualizat de `updateN`. Backend trimitere (worker, masina stari, idempotenta-logica, mapping-rezolvare) NEATINS — singura atingere de schema: 1 coloana nullable `override_json` cu migrare defensiva. PRD: [prd-3.6](prd/prd-3.6-editare-preview-acasa-unificata.md). | ISTORIC: 3.5 LIVRAT (dashboard compact). 11 stories in 4 valuri, TDD. US-001 bara status compacta pe 2 randuri cu bife accesibile (glife ✓/✗ + text, nu doar culoare) + `format_data_rar` (dd.mm.yyyy hh24:mi:ss, helper pur). US-002 Acasa = ecranul de import (upload dominant inline, tab Import scos, `?tab=import`→Acasa fara 404). US-003 helper pur partajat `app/payload_view.py` (payload→campuri afisabile, defensiv, coercion Excel) refolosit si de `GET /v1/prezentari` (DRY). US-004 "Coada"→"Trimiteri": coloane RO + stare umana + detaliu complet la click in panou dedicat `#trimitere-detaliu` (nu inline — poll 10s), scoped 404 cross-account. US-005/006 CRUD mapari operatii + formate coloane salvate (scoped, re-rezolvare auto la edit cod). US-007 "Mapari" 3 sectiuni (de rezolvat / op salvate / formate coloane), "Cont" doar cheie+creds. US-008 motiv (mesaj validare) pe randuri needs_data in preview. US-009 filtre Trimiteri (stare SQL / vehicul+data Python) scoped + "sterge filtrele". US-010 corectie inline needs_data→queued cu payload+idempotency recalculate, sent read-only (403), coliziune idempotency prinsa pre-UPDATE. US-011 badge contoare pe tab-uri (Mapari/Trimiteri), scoped, aria-label. VERIFY context curat PASS (483 teste; E2E browser/RAR LIVE neprobat — recomandata probare manuala `--send`). `/code-review` high a prins 4 findings reale, toate reparate: corectie needs_mapping re-rezolva prestatiile (nu mai poate trimite cod nul la RAR), filtru fara LIMIT silentios, coliziune idempotency atomica (try/except IntegrityError), comparatie data doar ISO. Backend trimitere (worker, masina stari, idempotenta-logica, mapping-rezolvare, schema) NEATINS. PRD: [prd-3.5](prd/prd-3.5-dashboard-compact-trimiteri-mapari.md). Urmeaza Etapa 4 (4.1 mapare AI/MCP). +**Ultima actualizare**: 2026-06-25 — 5.10 LIVRAT (UX trimiteri: pill filtre + paginare + detaliu; Mapari in meniu; branding ROMFAST + teme). 14 stories TDD prin echipa de workeri (lead orchestreaza, 3 teammates Sonnet pe valuri cu fisiere disjuncte; routes.py si base.html serializate ca fisiere fierbinti). US-001 fix filtrare data (`_iso_date_prefix` pe garda+comparatie). US-002 op service in `payload_view` (chei distincte `op_service_cod/denumire`, conventie goala `""`). US-003 pill-uri categorii `