From 1fbd8943292204f1f2646aedba339f68601ff028 Mon Sep 17 00:00:00 2001 From: Claude Agent Date: Tue, 23 Jun 2026 11:56:05 +0000 Subject: [PATCH] feat(web): uniformizare/standardizare UI/UX + lifecycle conturi (PRD 5.5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aduce toate suprafetele dashboard-ului la grila tabelului Trimiteri, muta navigarea intr-un meniu de cont (hamburger) si da panoului admin actiuni reale de ciclu de viata. 9 stories, 3 valuri. UI pur (reskin + reasezare) cu O SINGURA exceptie backend: modelul de stare a contului. - US-001 sectiunea "Ajutor" eliminata din Acasa (wayfinding redundant). - US-002 Nomenclator la grila standard (_submissions.html ca referinta). - US-003 macro autosend compact (Manual<->Auto). Semantica de PREZENTA `auto_send` (bifat->true, absent->false) NEALTERATA — compatibil cu ambele parsere (Form(bool) la /mapari, bool(form.get()) la import). Zero backend. - US-004 accounts.status (pending/active/blocked/archived/deleted), migrare defensiva idempotenta derivata din `active`, gate worker claim_one pe status='active' (echivalenta active=1 <=> status='active' pastrata). - US-005 tabel Mapari compact + panou Ajutor (
, proza o singura data), coloana "In coada". - US-006 meniu hamburger dropdown (Cont/Integrare/Nomenclator/Admin/logout) + context is_authenticated/is_admin/csrf_token defensiv in base.html. - US-007 tab-bar redus la Acasa+Mapari; rutele /_fragments/{cont,integrare, nomenclator} + deep-link ?tab= raman valide. - US-008 rute admin block/archive/delete + bulk pe lista account_id, require_admin + CSRF + PRG, dev id=1 sarit in bulk. - US-009 admin UI: selectie bife + master + bara bulk + kebab per-rand, grupare pe stare (bloc nou blocate/arhivate), nota "cont dev implicit" scoasa. Stergere = SOFT: tombstone (status='deleted'), dar PII purjata IMEDIAT (rar_creds_enc + chei API revocate + CUI eliberat pentru re-inregistrare), GDPR/L.142. VERIFY: 671 teste pass (+40). E2E browser (Playwright) a prins 2 bug-uri invizibile la TestClient: bara bulk cu display:flex inline invingea [hidden] (mutat in CSS .bulk-bar[hidden]); conturi arhivate cadeau sub "in asteptare" (grupare pe status). /code-review high a prins 2 bug-uri reale: soft delete pastra creds RAR + CUI la nesfarsit fara purjare accounts (GDPR neonorat); apostrof in numele firmei rupea confirm() inline din kebab — ambele reparate, plus cleanup boilerplate rute (_lifecycle_route). Backend trimitere (worker masina stari/idempotenta/mapping) neatins, cu exceptia gate-ului de cont. Design: docs/design/5.5-uniformizare-ui.md. Co-Authored-By: Claude Opus 4.8 (1M context) --- app/accounts.py | 69 ++++++- app/db.py | 12 ++ app/schema.sql | 7 + app/web/admin_routes.py | 78 ++++++-- app/web/routes.py | 1 + app/web/templates/_acasa.html | 11 +- app/web/templates/_macros.html | 44 ++--- app/web/templates/_mapari.html | 33 ++-- app/web/templates/_nomenclator.html | 12 +- app/web/templates/admin.html | 225 +++++++++++++--------- app/web/templates/base.html | 73 +++++++- app/web/templates/dashboard.html | 20 +- app/worker/__main__.py | 4 +- docs/ROADMAP.md | 3 +- docs/design/5.5-uniformizare-ui.md | 277 ++++++++++++++++++++++++++++ docs/prd/prd-5.5-uniformizare-ui.md | 229 +++++++++++++++++++++++ tests/test_acasa_trimiteri.py | 9 +- tests/test_account_status.py | 160 ++++++++++++++++ tests/test_accounts.py | 2 +- tests/test_admin_lifecycle.py | 137 ++++++++++++++ tests/test_autosend_toggle.py | 20 +- tests/test_web_admin.py | 128 +++++++++++++ tests/test_web_header_menu.py | 86 +++++++++ tests/test_web_labels.py | 11 +- tests/test_web_tabs.py | 34 +++- tests/test_web_uniformizare.py | 171 +++++++++++++++++ tests/test_worker_active_gate.py | 45 +++++ 27 files changed, 1700 insertions(+), 201 deletions(-) create mode 100644 docs/design/5.5-uniformizare-ui.md create mode 100644 docs/prd/prd-5.5-uniformizare-ui.md create mode 100644 tests/test_account_status.py create mode 100644 tests/test_admin_lifecycle.py create mode 100644 tests/test_web_admin.py create mode 100644 tests/test_web_header_menu.py create mode 100644 tests/test_web_uniformizare.py diff --git a/app/accounts.py b/app/accounts.py index 9781956..36e0103 100644 --- a/app/accounts.py +++ b/app/accounts.py @@ -39,9 +39,10 @@ def create_account( raise ValueError("name gol (un cont are nevoie de nume)") cui = _norm_cui(cui) try: + # Invariant (5.5): active=1 <=> status='active'; cont creat inactiv = 'pending'. cur = conn.execute( - "INSERT INTO accounts (name, cui, active) VALUES (?, ?, ?)", - (name, cui, 1 if active else 0), + "INSERT INTO accounts (name, cui, active, status) VALUES (?, ?, ?, ?)", + (name, cui, 1 if active else 0, "active" if active else "pending"), ) except sqlite3.IntegrityError: existing = conn.execute("SELECT id FROM accounts WHERE cui=?", (cui,)).fetchone() @@ -55,16 +56,72 @@ def create_account( def set_active(conn: sqlite3.Connection, account_id: int, active: bool) -> None: """Comuta `accounts.active`. Idempotent (set activ pe activ nu arunca). - Cont inexistent -> ValueError.""" + Cont inexistent -> ValueError. + + Mentine invariantul 5.5 active=1 <=> status='active': activarea -> 'active', + dezactivarea -> 'pending' (legacy „in asteptare"). Pentru blocare/arhivare/stergere + foloseste `set_status`/`delete_account`. + """ row = conn.execute("SELECT 1 FROM accounts WHERE id=?", (account_id,)).fetchone() if not row: raise ValueError(f"cont inexistent: {account_id}") - conn.execute("UPDATE accounts SET active=? WHERE id=?", (1 if active else 0, account_id)) + conn.execute( + "UPDATE accounts SET active=?, status=? WHERE id=?", + (1 if active else 0, "active" if active else "pending", account_id), + ) + + +# Stari de ciclu de viata gestionate explicit (5.5). 'deleted' = stergere soft (purjata de +# retentie); restul sunt reversibile. +VALID_STATUSES = ("pending", "active", "blocked", "archived", "deleted") +# Verbele care nu se pot aplica contului de sistem id=1 (protejat, ca la deactivate in 3.3b). +_PROTECTED_ACCOUNT_ID = 1 + + +def set_status(conn: sqlite3.Connection, account_id: int, status: str) -> None: + """Seteaza `accounts.status` la una din `VALID_STATUSES`, mentinand mirror-ul `active` + (active=1 doar pentru 'active', altfel 0). + + Contul de sistem id=1 NU poate fi mutat din 'active' (cont default) -> ValueError. + Status invalid sau cont inexistent -> ValueError. + """ + if status not in VALID_STATUSES: + raise ValueError(f"status invalid: {status}") + row = conn.execute("SELECT 1 FROM accounts WHERE id=?", (account_id,)).fetchone() + if not row: + raise ValueError(f"cont inexistent: {account_id}") + if account_id == _PROTECTED_ACCOUNT_ID and status != "active": + raise ValueError("Contul default (id=1) nu poate fi blocat/arhivat/sters (cont de sistem).") + conn.execute( + "UPDATE accounts SET active=?, status=? WHERE id=?", + (1 if status == "active" else 0, status, account_id), + ) + + +def delete_account(conn: sqlite3.Connection, account_id: int) -> None: + """Stergere SOFT: randul ramane ca tombstone (status='deleted', scos din liste), DAR datele + sensibile se purjeaza IMEDIAT (GDPR/L.142): credentialele RAR criptate sterse, cheile API + revocate si CUI-ul eliberat (ca acelasi CUI sa se poata re-inregistra — altfel indexul unic + `ux_accounts_cui` l-ar tine blocat de un cont invizibil). Contul de sistem id=1 e protejat. + + Nota: nu facem hard DELETE pe rand din cauza FK-urilor (submissions/api_keys/...); pastram + tombstone-ul pentru audit, dar fara PII. Jobul de retentie T16 purjeaza `submissions`/batches, + NU acest tombstone — de aceea purjam PII aici, la momentul stergerii.""" + set_status(conn, account_id, "deleted") # valideaza existenta + protejeaza id=1; seteaza status+active=0 + conn.execute( + "UPDATE accounts SET rar_creds_enc=NULL, cui=NULL WHERE id=?", (account_id,) + ) + conn.execute( + "UPDATE api_keys SET active=0, revoked_at=datetime('now') WHERE account_id=? AND active=1", + (account_id,), + ) def list_accounts(conn: sqlite3.Connection) -> list[dict]: - """Metadate conturi (FARA `rar_creds_enc`), ordonate dupa id.""" + """Metadate conturi (FARA `rar_creds_enc`), ordonate dupa id. Exclude conturile 'deleted' + (stergere soft -> invizibile in panou).""" rows = conn.execute( - "SELECT id, name, cui, active, created_at FROM accounts ORDER BY id" + "SELECT id, name, cui, active, status, created_at FROM accounts " + "WHERE status != 'deleted' ORDER BY id" ).fetchall() return [dict(r) for r in rows] diff --git a/app/db.py b/app/db.py index 1bce0f8..66e9abe 100644 --- a/app/db.py +++ b/app/db.py @@ -63,6 +63,18 @@ def _migrate(conn: sqlite3.Connection) -> None: if "active" not in acc_cols: # Conturi existente raman active (default 1). Lifecycle consumat de 3.3. conn.execute("ALTER TABLE accounts ADD COLUMN active INTEGER NOT NULL DEFAULT 1") + acc_cols.add("active") + if "status" not in acc_cols: + # Stare de ciclu de viata (5.5). Defensiv idempotent (ca is_admin in 3.3b). + # Default 'active' (trece CHECK pe randurile existente), apoi derivam din `active`: + # active=0 -> 'pending'. Invariant: active=1 <=> status='active'. + conn.execute( + "ALTER TABLE accounts ADD COLUMN status TEXT NOT NULL DEFAULT 'active' " + "CHECK (status IN ('pending','active','blocked','archived','deleted'))" + ) + conn.execute( + "UPDATE accounts SET status='pending' WHERE active=0 AND status='active'" + ) # Unicitate CUI (un CUI = un cont); NULL distinct nativ -> conturi fara CUI multiplu. conn.execute( "CREATE UNIQUE INDEX IF NOT EXISTS ux_accounts_cui ON accounts(cui) WHERE cui IS NOT NULL" diff --git a/app/schema.sql b/app/schema.sql index 1fb5e60..720dfc4 100644 --- a/app/schema.sql +++ b/app/schema.sql @@ -11,6 +11,13 @@ CREATE TABLE IF NOT EXISTS accounts ( name TEXT NOT NULL, cui TEXT, active INTEGER NOT NULL DEFAULT 1, -- lifecycle cont (3.1); gate „in asteptare" consumat de 3.3 + -- Stare de ciclu de viata explicita (5.5). Superset al lui `active`: mentinem invariantul + -- active=1 <=> status='active' (vezi accounts.set_status / set_active). Worker gate-uieste pe status. + -- pending=neactivat · active=operational · blocked=suspendat reversibil · archived=scos din liste, + -- date read-only · deleted=stergere soft (tombstone; PII/creds + CUI purjate imediat la stergere, + -- vezi accounts.delete_account — randul ramane doar pentru audit). + status TEXT NOT NULL DEFAULT 'active' + CHECK (status IN ('pending','active','blocked','archived','deleted')), rar_creds_enc TEXT, -- creds RAR criptate (Fernet) durabile per-cont (D4/Eng#1) created_at TEXT NOT NULL DEFAULT (datetime('now')) ); diff --git a/app/web/admin_routes.py b/app/web/admin_routes.py index 02cf0d1..de6eb68 100644 --- a/app/web/admin_routes.py +++ b/app/web/admin_routes.py @@ -15,7 +15,7 @@ from fastapi.responses import HTMLResponse, RedirectResponse from fastapi.templating import Jinja2Templates from .. import __version__ -from ..accounts import list_accounts, set_active +from ..accounts import list_accounts, set_active, set_status, delete_account from ..config import get_settings from ..db import get_connection from ..web.csrf import get_csrf_token, verify_csrf @@ -49,16 +49,20 @@ def _render_admin(request: Request, conn, *, error: str | None = None, status_co emails = _emails_by_account(conn) for acct in accounts: acct["email"] = emails.get(acct["id"]) - pending = [a for a in accounts if not a["active"] and a["id"] != 1] - active = [a for a in accounts if a["active"] and a["id"] != 1] - default = next((a for a in accounts if a["id"] == 1), None) + # Grupare pe STARE (5.5), nu pe `active`: altfel conturile arhivate/blocate (active=0) + # ar cadea gresit sub "in asteptare". 'deleted' e deja exclus din list_accounts. + pending = [a for a in accounts if a["status"] == "pending" and a["id"] != 1] + active = [a for a in accounts if a["status"] == "active" and a["id"] != 1] + suspended = [a for a in accounts if a["status"] in ("blocked", "archived") and a["id"] != 1] return _TMPL.TemplateResponse(request, "admin.html", _ctx( request, csrf_token=get_csrf_token(request), pending=pending, active=active, - default_account=default, + suspended=suspended, error=error, + is_authenticated=True, + is_admin=True, ), status_code=status_code) @@ -74,28 +78,66 @@ async def admin_get(request: Request): conn.close() -@router.post("/admin/activate", response_class=HTMLResponse) -async def admin_activate( - request: Request, - account_id: int = Form(...), - csrf_token: str = Form(default=""), -): - """Activeaza un cont. PRG: redirect 303 la /admin dupa succes.""" +def _apply_lifecycle(conn, ids: list[int], action: str) -> None: + """Aplica un verb de ciclu de viata (5.5) pe o lista de conturi. Conturile protejate + (id=1) sau inexistente ridica ValueError din helperi -> sarite (nu opresc bulk-ul). + `action`: activate | block | archive | delete.""" + for aid in ids: + try: + if action == "activate": + set_status(conn, aid, "active") + elif action == "block": + set_status(conn, aid, "blocked") + elif action == "archive": + set_status(conn, aid, "archived") + elif action == "delete": + delete_account(conn, aid) + except ValueError: + continue # cont de sistem / inexistent -> sarit + + +def _lifecycle_route(request: Request, account_id: list[int], csrf_token: str, action: str): + """Corp comun pentru rutele de ciclu de viata (5.5): auth + CSRF + aplica verbul (bulk) + PRG. + Evita 4 handlere copy-paste care difera doar prin verb.""" require_admin(request) verify_csrf(request, csrf_token) - conn = get_connection() try: - try: - set_active(conn, account_id, True) - except ValueError as exc: - return _render_admin(request, conn, error=str(exc), status_code=422) + _apply_lifecycle(conn, account_id, action) + conn.commit() finally: conn.close() - return RedirectResponse("/admin", status_code=303) +@router.post("/admin/activate", response_class=HTMLResponse) +async def admin_activate(request: Request, account_id: list[int] = Form(...), + csrf_token: str = Form(default="")): + """Activeaza unul sau mai multe conturi (bulk). PRG 303.""" + return _lifecycle_route(request, account_id, csrf_token, "activate") + + +@router.post("/admin/block", response_class=HTMLResponse) +async def admin_block(request: Request, account_id: list[int] = Form(...), + csrf_token: str = Form(default="")): + """Blocheaza (suspendare reversibila) unul sau mai multe conturi. PRG 303.""" + return _lifecycle_route(request, account_id, csrf_token, "block") + + +@router.post("/admin/archive", response_class=HTMLResponse) +async def admin_archive(request: Request, account_id: list[int] = Form(...), + csrf_token: str = Form(default="")): + """Arhiveaza (scos din listele active, date read-only) unul sau mai multe conturi. PRG 303.""" + return _lifecycle_route(request, account_id, csrf_token, "archive") + + +@router.post("/admin/delete", response_class=HTMLResponse) +async def admin_delete(request: Request, account_id: list[int] = Form(...), + csrf_token: str = Form(default="")): + """Stergere SOFT (tombstone + purjare PII imediata) a unuia sau mai multor conturi. PRG 303.""" + return _lifecycle_route(request, account_id, csrf_token, "delete") + + @router.post("/admin/deactivate", response_class=HTMLResponse) async def admin_deactivate( request: Request, diff --git a/app/web/routes.py b/app/web/routes.py index 0750061..9d31dad 100644 --- a/app/web/routes.py +++ b/app/web/routes.py @@ -331,6 +331,7 @@ def dashboard(request: Request, tab: str = "acasa") -> HTMLResponse: "active_tab": active_tab, "panel_html": panel_html, "badges": badges, + "is_authenticated": True, "is_admin": is_account_admin(conn, account_id), "csrf_token": get_csrf_token(request), } diff --git a/app/web/templates/_acasa.html b/app/web/templates/_acasa.html index 2c9b700..17130a4 100644 --- a/app/web/templates/_acasa.html +++ b/app/web/templates/_acasa.html @@ -44,15 +44,8 @@ {% endif %} - {# === Subordonat: ajutor rapid pe un rand discret === - US-003 (3.6): linkul redundant "Trimiteri" a fost scos (Trimiterile sunt mai jos - pe aceeasi pagina). Wayfinding "Mapari"/"Coduri RAR" pastrat pentru operatori. #} -
- Ajutor: - Mapari - Coduri RAR -
+ {# US-001 (5.5): randul "Ajutor" (wayfinding Mapari/Coduri RAR) eliminat — navigarea + traieste in tab-bar (Mapari) si in meniul de cont (Nomenclator etc.). #} {# === Sectiunea Trimiteri ("Trimiterile tale"), permanenta sub upload (US-003). Suprimata la first-run (zero trimiteri): bara de upload acopera deja CTA-ul, diff --git a/app/web/templates/_macros.html b/app/web/templates/_macros.html index dca404f..14d9ea5 100644 --- a/app/web/templates/_macros.html +++ b/app/web/templates/_macros.html @@ -1,26 +1,28 @@ {# Macro-uri partajate intre template-urile de import si mapari. #} -{# US-007: comutator pe COADA in loc de bifa "auto-send". - Framing pe punerea in coada, NU pe trimitere (poarta autoplan UC-A): etichetele - poarta singure sensul de send-safety. `name="auto_send" value="true"` pastrat cu - semantica de prezenta (bifat -> True, nebifat -> absent -> False) ca sa produca - bool corect cu AMBELE parsere backend (Form(bool) la /mapari, bool(form.get()) - la /_import/.../mapare-operatie). Zero atingere backend. +{# US-003 (5.5): comutator COMPACT "In coada" — Manual <-> Auto pe un singur rand, fara + proza inline (explicatia traieste o singura data in panoul Ajutor al cardului Mapari). + Inlocuieste varianta verbose din 3.6 care injecta 3 randuri de text pe FIECARE linie de + tabel (randuri inalte -> Salveaza/Sterge ieseau din viewport). + + INVARIANT BACKEND (nealterat din 3.6): control = checkbox cu `name="auto_send" value="true"` + si SEMANTICA DE PREZENTA (bifat -> trimite "true" -> True; nebifat -> camp absent -> False). + E singura forma compatibila cu AMBELE parsere: `Form(bool)` la /mapari SI `bool(form.get())` + la /_import/.../mapare-operatie. Radio Auto/Manual cu value="false" ar trimite campul prezent + pe "Manual" -> `bool("false")` = True la import (regresie tacuta). De aceea comutator vizual + Manual<->Auto peste checkbox, NU doua radio-uri. Zero atingere backend. - form_id: leaga input-ul de un
extern (necesar in celulele de tabel). - - checked: starea initiala (H4 - reflecta valoarea STOCATA per mapare). #} + - checked: starea STOCATA per mapare (H4) — bifat = Auto. #} {% macro autosend_toggle(form_id='', checked=True) -%} -
- La fisierele viitoare cu aceasta operatie: - - - Nebifat = "Tine pentru verificare". Doar pentru aceasta operatie; - nimic nu pleaca la RAR pana confirmi. - -
+ {%- endmacro %} diff --git a/app/web/templates/_mapari.html b/app/web/templates/_mapari.html index 8a8ac6e..eb1e409 100644 --- a/app/web/templates/_mapari.html +++ b/app/web/templates/_mapari.html @@ -9,7 +9,22 @@
-

De rezolvat

+ {# 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. #} +

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 %}
@@ -17,18 +32,13 @@ Importa un fisier nou daca vrei sa adaugi prezentari.
{% else %} -

- Operatii ROAAUTO necunoscute, blocate in needs_mapping. - Alege codul RAR (sugestia fuzzy e preselectata) si salveaza — submission-urile se deblocheaza automat. -

-
- + @@ -88,17 +98,14 @@ Nicio mapare salvata inca. Pe masura ce mapezi operatii, ele apar aici si le poti edita oricand. {% else %} -

- Maparile operatie -> cod RAR retinute pentru contul tau. Schimba codul sau punerea in coada si salveaza; - la schimbarea unui cod, submission-urile blocate pe acea operatie se re-rezolva automat. -

+ {# US-005 (5.5): proza explicativa mutata in panoul Ajutor de la "De rezolvat" (o singura data). #}
Operatie Sugestii Cod RARPunere in coadaIn coada
- + @@ -160,7 +167,7 @@ {% else %}

- Antetele de fisier recunoscute. Un fisier cu alte coloane = format nou separat (nu suprascrie). + Antetele de fisier recunoscute. Un fisier cu alte coloane = format nou separat.

diff --git a/app/web/templates/_nomenclator.html b/app/web/templates/_nomenclator.html index 629a97d..391b365 100644 --- a/app/web/templates/_nomenclator.html +++ b/app/web/templates/_nomenclator.html @@ -1,12 +1,20 @@ +{# US-002 (5.5): aceeasi grila standard ca tabelul Trimiteri (_submissions.html): + .tablewrap > table, antet th standard (mostenit din base.html), cod in .pill, + denumire ca text normal (singura coloana care se poate rupe pe randuri inguste), + empty-state in .empty. Zero stiluri inline noi — totul vine din base.html. #} {% if rows %}
Operatie Cod RARPunere in coadaIn coada Actiuni
- + + + + + {% for r in rows %} - + {% endfor %} diff --git a/app/web/templates/admin.html b/app/web/templates/admin.html index 4bf6678..41596e0 100644 --- a/app/web/templates/admin.html +++ b/app/web/templates/admin.html @@ -1,6 +1,104 @@ {% extends "base.html" %} {% block title %}Panou admin — Gateway RAR AUTOPASS{% endblock %} {% block content %} + +{# US-009 (5.5): metadate verbe de ciclu de viata (eticheta, ruta, clasa). #} +{% set VERBS = { + 'activate': ('Activeaza', '/admin/activate', ''), + 'block': ('Blocheaza', '/admin/block', ''), + 'archive': ('Arhiveaza', '/admin/archive', ''), + 'delete': ('Sterge', '/admin/delete', 'danger') +} %} + +{% macro lifecycle_block(title, rows, block_id, bulk_verbs, row_verbs) %} +
+

{{ title }} ({{ rows|length }})

+ {% if rows %} + {# Bara bulk: form propriu (id=bulk-); checkbox-urile randurilor se leaga prin atributul + HTML5 form= (fara form-uri imbricate). Ascunsa pana exista o selectie (JS). #} + + + + + +
+
CodDenumireActualizat
CodDenumireActualizat
{{ r.cod_prestatie }}{{ r.nume_prestatie }}{{ r.nume_prestatie }} {{ r.updated_at }}
+ + + + + + {% for acct in rows %} + + + + + + + + + + + {% endfor %} + +
IDCompanieCUIEmailStareInregistratActiuni
{{ acct.id }}{{ acct.name }}{{ acct.cui or "—" }}{{ acct.email or "—" }}{{ acct.status }}{{ acct.created_at or "—" }} +
+ +
+ {% for v in row_verbs %} + {% set label, action, cls = VERBS[v] %} + {# Confirm fara nume interpolat: un apostrof in numele firmei (free-form) ar rupe + string-ul JS din atributul inline (entitatea ' e decodata inainte de parse). #} +
+ + + +
+ {% endfor %} +
+
+
+
+ {% else %} +

Niciun cont.

+ {% endif %} +
+{% endmacro %} + + +

Panou admin

Inapoi la dashboard @@ -10,96 +108,45 @@ {% endif %} - -
-

Conturi in asteptare ({{ pending|length }})

- {% if pending %} -
- - - - - - - - - - - - - {% for acct in pending %} - - - - - - - - - {% endfor %} - -
IDCompanieCUIEmailInregistratActiune
{{ acct.id }}{{ acct.name }}{{ acct.cui or "—" }}{{ acct.email or "—" }}{{ acct.created_at or "—" }} -
- - - -
-
-
- {% else %} -

Niciun cont in asteptare.

- {% endif %} -
+{{ lifecycle_block("Conturi in asteptare", pending, "pending", + ['activate', 'block', 'archive', 'delete'], + ['activate', 'block', 'archive', 'delete']) }} - -
-

Conturi active ({{ active|length }})

- {% if active %} -
- - - - - - - - - - - - - {% for acct in active %} - - - - - - - - - {% endfor %} - -
IDCompanieCUIEmailInregistratActiune
{{ acct.id }}{{ acct.name }}{{ acct.cui or "—" }}{{ acct.email or "—" }}{{ acct.created_at or "—" }} -
- - - -
-
-
- {% else %} -

Niciun cont activ (in afara de contul dev).

- {% endif %} -
+{{ lifecycle_block("Conturi active", active, "active", + ['block', 'archive', 'delete'], + ['block', 'archive', 'delete']) }} - -{% if default_account %} -
-

- Cont dev implicit (id=1): {{ default_account.name }} - — activ={{ default_account.active }} — fara buton de activare/dezactivare (cont de sistem). -

-
-{% endif %} +{# Conturi suspendate (blocate/arhivate): reactivare sau stergere. Stare reala in pill. #} +{{ lifecycle_block("Conturi blocate / arhivate", suspended, "suspended", + ['activate', 'delete'], + ['activate', 'delete']) }} + + {% endblock %} diff --git a/app/web/templates/base.html b/app/web/templates/base.html index d2e3c75..ab9285a 100644 --- a/app/web/templates/base.html +++ b/app/web/templates/base.html @@ -116,6 +116,22 @@ .eroare-3n-label { font-weight:500; } /* Inline fix per camp in preview */ .camp-fix { color:var(--accent); font-size:11px; margin-top:2px; display:block; } + /* Meniu hamburger cont (US-006 PRD 5.5) — dropdown ancorat dreapta-sus */ + .cont-menu-wrap { position:relative; } + .icon-btn { background:transparent; border:1px solid var(--line); color:var(--ink); cursor:pointer; + border-radius:6px; min-height:36px; min-width:36px; font-size:16px; padding:4px 8px; + line-height:1; display:inline-flex; align-items:center; justify-content:center; } + .icon-btn:hover { background:var(--line); } + .cont-menu { position:absolute; right:0; top:calc(100% + 8px); min-width:180px; z-index:50; + background:var(--card); border:1px solid var(--line); border-radius:8px; padding:6px; + box-shadow:0 8px 24px rgba(0,0,0,.18); display:flex; flex-direction:column; gap:2px; } + .cont-menu[hidden] { display:none; } + .cont-menu a, .cont-menu button { display:block; width:100%; text-align:left; background:transparent; + border:none; color:var(--ink); text-decoration:none; font:inherit; padding:8px 10px; + border-radius:6px; cursor:pointer; min-height:36px; } + .cont-menu a:hover, .cont-menu button:hover { background:var(--line); } + .cont-menu hr { border:none; border-top:1px solid var(--line); margin:4px 0; } + .cont-menu form { margin:0; } @@ -123,11 +139,30 @@

Gateway RAR AUTOPASS

{{ rar_env }}
- + title="Comuta tema">☀ v{{ version }} + {% if is_authenticated|default(false) %} + {# Meniu cont (US-006 PRD 5.5): Cont/Integrare/Nomenclator + (admin) + logout. + Pe paginile neautentificate (login/signup) nu se randeaza deloc. #} +
+ + +
+ {% endif %}
{% block content %}{% endblock %}
@@ -165,5 +200,37 @@ }); })(); + diff --git a/app/web/templates/dashboard.html b/app/web/templates/dashboard.html index 183ef90..1818213 100644 --- a/app/web/templates/dashboard.html +++ b/app/web/templates/dashboard.html @@ -1,14 +1,8 @@ {% extends "base.html" %} {% block content %} - -
- {% if is_admin %}Panou admin{% endif %} -
- - -
-
+{# US-007 (5.5): nav-ul ad-hoc (Panou admin + logout) a fost mutat in meniul de cont (☰) + din header (base.html). Aici raman doar bara de status + tab-bar-ul de lucru zilnic. #}
- {# US-003 (3.6): tab-ul "Trimiteri" (coada) a fost eliminat — Trimiterile traiesc - ca sectiune permanenta pe Acasa. Raman: Acasa·Mapari·Cont·Nomenclator·Integrare. #} + {# US-007 (5.5): tab-bar redus la suprafetele de LUCRU ZILNIC (Acasa·Mapari). + Cont/Integrare/Nomenclator s-au mutat in meniul de cont (☰) din header — rutele + `/_fragments/{cont,integrare,nomenclator}` + deep-link `?tab=` raman valide. #} {% set tabs = [ ("acasa", "Acasa", "tab-acasa"), - ("mapari", "Mapari", "tab-mapari"), - ("cont", "Cont", "tab-cont"), - ("nomenclator", "Nomenclator", "tab-nomenclator"), - ("integrare", "Integrare", "tab-integrare") + ("mapari", "Mapari", "tab-mapari") ] %} {% for tab_id, tab_label, tab_elem_id in tabs %} {% set badge = (badges.get(tab_id, 0) if badges else 0) %} diff --git a/app/worker/__main__.py b/app/worker/__main__.py index 9a73aed..21d000e 100644 --- a/app/worker/__main__.py +++ b/app/worker/__main__.py @@ -145,7 +145,9 @@ def claim_one(conn) -> dict | None: "FROM submissions s LEFT JOIN accounts a ON a.id = s.account_id " "WHERE s.status='queued' " "AND (s.next_attempt_at IS NULL OR s.next_attempt_at <= ?) " - "AND COALESCE(a.active, 1) = 1 " + # Gate pe stare de cont (5.5): doar 'active' trimite. Derivam defensiv din `active` + # cand `status` lipseste (DB veche pre-migrare), pastrand active=1 <=> 'active'. + "AND COALESCE(a.status, CASE WHEN COALESCE(a.active,1)=1 THEN 'active' ELSE 'pending' END) = 'active' " "ORDER BY s.id LIMIT 1", (_iso(_now()),), ).fetchone() diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 5fe93ce..de56dcb 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-22 — 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-23 — 5.5 APROBAT + IN EXECUTIE (uniformizare/standardizare UI/UX: tabele la grila Trimiteri, meniu hamburger + tab-bar redus Acasa/Mapari, panou admin cu selectie+bulk pe model nou `accounts.status`. 9 stories in 3 valuri, UI pur cu o singura exceptie backend = stare cont. Design+PRD aprobate, 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). > 2026-06-18 — 3.4 LIVRAT (interfata web ergonomica: tab-uri + wizard + microcopy). US-001 modul pur `app/web/labels.py` (stari tehnice→text uman + clasa CSS; test parametrizat din CHECK-ul `schema.sql` iese rosu la stare nemapata). US-002 bara status `/_fragments/status` + `_status.html` (etichete umane, defalcare blocate pe motiv, poll 15s, scoped pe cont). US-003 shell 6 tab-uri (Acasa·Import·Coada·Mapari·Cont·Nomenclator) cu deep-link `?tab=`, panou activ randat server-side, fragmente inactive lazy pe click, ARIA real (tablist/tab/tabpanel + aria-selected + navigare cu sageti). US-004 stepper import 4 pasi (PUR vizual, `hx-target="#import-section"` + csrf pastrate). US-005 Acasa onboarding checklist auto-bifat (are_creds/are_trimiteri) + colaps cand totul gata + empty states prietenoase Coada/Mapari. VERIFY lead-driven (TestClient ACs + 434 pytest pass; E2E browser/RAR LIVE neprobat in sesiune — recomandata probare manuala `--send`). Fix izolare teste (reset `ratelimit._hits` in fixturi, 429 la rulare subset). `/code-review` high: regasit avertisment "cont in asteptare de activare" (regresie din scoaterea `/_fragments/banner`) re-introdus in bara status + culori hardcodate→variabile paleta. 434 teste pass. Backend trimitere neatins. PRD: [prd-3.4](prd/prd-3.4-ux-dashboard-web.md). Urmeaza Etapa 4 (4.1 mapare AI/MCP). @@ -99,6 +99,7 @@ Reguli de contract (detalii in `docs/api-rar-contract.md`): `FINALIZATA` e termi | 5.1 | Hub de integrare (pagina `/integrare` autentificata): exemple cod multi-limbaj (curl/Python/PHP/C#/Node) + retetar **Visual FoxPro** (POST via `MSXML2.ServerXMLHTTP` + upload CSV) + export OpenAPI/Postman + buton "Testeaza conexiunea" | DONE | 2026-06-22 | 4 stories (2 valuri, 2 echipe paralel + restore dupa clobber de merge). US-001 `GET /v1/ping` readiness (`account_id/mediu/autentificat_cu_cheie/are_creds_rar/ts`) + `GET /v1/integrare/postman.json` (v2.1.0, allowlist 3 rute). US-002 `integrare_examples.py` pur (7 limbaje × 2 canale, drift-test `is_required()`). US-003 tab "Integrare" IA pe 2 niveluri (limbaj→canal, VFP cu dialecte MSXML2/WinHttp), copy din `
`, empty-state CTA, export `.cardlink`, doar tokens. US-004 `POST /integrare/test-cheie` (`account_for_key` direct, scoped sesiune, no-echo). VERIFY: 568 teste + E2E browser (Playwright: VFP 3 niveluri comuta, htmx test-cheie → fragment eroare, 0 erori consola) + enqueue live; live RAR neprobat (lipsa creds key). `/code-review` high: 4 bug-uri reale reparate (C#/VFP snippet JSON multi-linie nevalid, Node `node:buffer` fara FormData, script ne-scoped acumuland listeneri). Backend trimitere NEATINS. PRD: [prd-5.1](prd/prd-5.1-hub-integrare.md) |
 | 5.2 | Endpoint dry-run `POST /v1/prezentari/valideaza` — valideaza payload + mapare, intoarce erorile reale FARA enqueue | DONE | 2026-06-22 | 1 story (US-001), un worker TDD. Helper pur partajat `classify_prezentare` (mapping.py) folosit de AMBELE rute → garanteaza ca dry-run-ul da acelasi verdict ca trimiterea reala (invariant de corectitudine, nu doar DRY); `create_prezentari` refactorizat pe el (comportament identic, test_api.py verde). Ruta read-only: `{results:[{index,valid,status_estimat,erori,nemapate,prestatii_rezolvate}]}`, `rar_credentials` optional+ignorat, zero scriere DB, scope prin `resolve_account_id`. Doar validare+mapare (FARA idempotency/duplicat — decizie user; `idempotency.py` neatins) + hub `/integrare` amanat. VERIFY context curat PASS (577 teste; E2E API: queued/needs_data/needs_mapping + COUNT(*)=0 dupa dry-run, fara leak creds; regresia de aur `POST /v1/prezentari`→queued verde; live RAR neprobat — lipsa `AUTOPASS_CREDS_KEY`/creds test, endpoint read-only nu atinge worker/coada). `/code-review` high: 0 findings. PRD: [prd-5.2](prd/prd-5.2-dryrun-valideaza.md) |
 | 5.3 | Light/Dark mode — toggle in header, persistat (localStorage); CSS deja pe variabile `:root` | DONE | 2026-06-22 | 3 stories (US-001 paleta light + US-003 fix suprafete fragmente; US-002 comutator+persistenta+anti-FOUC), un worker TDD secvential (toate ating templates). Tema light = bloc `[data-theme="light"]` care suprascrie variabilele `:root` (dark NESCHIMBAT la octet, default pastrat); fundalurile de stare hardcodate (`#241a1a`/`#201c0f`/`#16241c`) convertite la `color-mix(... 12%, var(--card))` in `base.html` + 7 fragmente `_*.html`. Comutator soare/luna in header (aria-label, >=36px), pe toate paginile (login/signup/dashboard/admin). Default OS-aware (`prefers-color-scheme`, fallback dark); persistenta `localStorage` DOAR la click (init doar sincronizeaza iconita → OS-aware ramane viu pana la comutare explicita); script anti-FOUC in `` inainte de `