feat(web): UI compact Trimiteri + bara bulk contextuala + fix buton "+" editor (PRD 5.22)

Trimiteri desktop = tabel grid cu header, un rand per trimitere: status =
bulina cu tooltip, mediu = litera T/P, multi-cod = primul cod + badge +N,
sub-linie de eroare cu textul brut RAR doar pe error/needs_data. Mobil:
2 linii prin grid-areas, fara header. Actiunile bulk apar doar la selectie
(bara contextuala cu contor + texte explicite). Mapari: exemplu concret in
loc de descriere la Reguli automate.

Fix buton "+" chips (nu facea nimic, fara eroare): hx-disabled-elt="find
button" mostenit de la form-ul parinte crapa htmx inainte de request ->
hx-disabled-elt="this" explicit pe butoanele chips; separat, no-op-urile
silentioase din post_form_chips au acum mesaje (cod invalid/duplicat) si
confirmare cu chip evidentiat la succes.

Suita: 1622 passed. Verify E2E in browser (desktop 1280 + mobil 390).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-07-07 12:27:42 +00:00
parent 6674d0e3e1
commit 5a2c976d46
17 changed files with 912 additions and 169 deletions

View File

@@ -59,6 +59,19 @@ def _vin_scurt(vin: str) -> str:
return "" + vin[-6:]
def _coduri_rar(prestatii: Any) -> list[str]:
"""Codurile RAR (cod_prestatie) din toate prestatiile: ordonate, dedup."""
coduri: list[str] = []
if isinstance(prestatii, list):
for item in prestatii:
if not isinstance(item, dict):
continue
cod = _clean_cod_rar(item.get("cod_prestatie"))
if cod and cod not in coduri:
coduri.append(cod)
return coduri
def _prima_prestatie(prestatii: Any) -> dict[str, Any]:
"""Primul item de prestatie ca dict, defensiv (lista goala/non-dict -> {})."""
if isinstance(prestatii, list):
@@ -83,10 +96,11 @@ def _payload_dict(payload: str | dict | None) -> dict[str, Any]:
return {}
def prezentare_din_payload(payload: str | dict | None) -> dict[str, str]:
def prezentare_din_payload(payload: str | dict | None) -> dict[str, Any]:
"""Campuri afisabile dintr-un payload de submission.
Intoarce un dict cu chei stabile (toate string-uri, EMPTY cand lipsesc):
Intoarce un dict cu chei stabile (string-uri, EMPTY cand lipsesc; exceptie
`coduri_rar` = list[str]):
vehicul_nr, vin, vin_scurt, operatie, data_prestatie, odometru, cod.
`operatie` = denumire prestatiei daca exista, altfel codul; `cod` = codul RAR
@@ -138,6 +152,9 @@ 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,
# Lista ordonata, fara duplicate, a codurilor RAR din TOATE prestatiile
# (goala cand nimic nu e mapat). Randul compact afiseaza primul + "+N".
"coduri_rar": _coduri_rar(data.get("prestatii")),
# Chei cu conventie goala "" (nu EMPTY) — vezi comentariu de mai sus
"op_service_cod": op_service_cod,
"op_service_denumire": op_service_denumire,

View File

@@ -359,6 +359,31 @@ def parse_erori(rar_error: object) -> list[dict]:
return [{"problema": str(data)[:200], "cauza": "", "fix": "", "field": None}]
# ---------------------------------------------------------------------------
# eroare_bruta — textul compact al erorii pentru sub-linia randului de tabel
# ---------------------------------------------------------------------------
# Stari cu sub-linie de eroare pe rand. needs_mapping e EXCLUS intentionat:
# "nemapat" e deja vizibil in coloana Cod RAR, sub-linia ar fi redundanta.
_STARI_CU_EROARE_BRUTA = ("error", "needs_data")
def eroare_bruta(status: str, rar_error: object) -> str:
"""Textul brut, compact, al primei erori — pentru sub-linia randului.
Prefera cauza tehnica (mesajul serverului RAR, ex. ORA-12899) si cade pe
problema. Sir gol in afara starilor error/needs_data. Nu arunca niciodata
(reuse parse_erori, degradare gratioasa).
"""
if status not in _STARI_CU_EROARE_BRUTA:
return ""
erori = parse_erori(rar_error)
if not erori:
return ""
e = erori[0]
return (str(e.get("cauza") or "").strip() or str(e.get("problema") or "").strip())[:200]
# ---------------------------------------------------------------------------
# Etichete stari preview import (vocabular DIFERIT de starile de submission)
#

View File

@@ -32,6 +32,7 @@ from ..web.csrf import get_csrf_token, verify_csrf
from .labels import (
ETICHETA_ULTIMA_AUTENTIFICARE_RAR,
STARI_PREVIEW,
eroare_bruta,
eticheta_env,
eticheta_rar,
eticheta_scurta,
@@ -1131,6 +1132,8 @@ def _submission_row_view(r) -> dict:
"motiv": motiv,
# eticheta umana scurta a problemei sub pill (text, nu cod brut).
"eticheta_problema": _eticheta_problema(r["status"], motiv),
# textul brut compact al erorii (mesaj RAR/validare) — sub-linia randului.
"eroare_bruta": eroare_bruta(r["status"], r["rar_error"]),
# randurile blocate (error/needs_data/needs_mapping) sunt selectabile pentru
# stergere bulk; sent/sending/queued raman read-only (fara checkbox).
"gestionabil": r["status"] in _GESTIONABILE_WEB,
@@ -2397,9 +2400,19 @@ async def post_form_chips(request: Request) -> HTMLResponse:
})
action = str(form.get("chips_action") or "").strip()
chips_extra_error = False # T-C1/T-E4 (5.16): semnal pentru add_extra esuat
# Orice adaugare are un efect vizibil: mesaj de refuz (chips_error) sau
# confirmare + chip evidentiat (chips_ok / chips_added_cod). Fara no-op silentios.
chips_error = ""
chips_ok = ""
chips_added_cod = ""
conn = get_connection()
def _cod_valid(cod: str) -> bool:
return conn.execute(
"SELECT 1 FROM nomenclator_rar WHERE cod_prestatie=?", (cod,)
).fetchone() is not None
try:
if action == "add":
# Adauga cod la operatia specificata prin chips_add_op_index
@@ -2408,44 +2421,37 @@ async def post_form_chips(request: Request) -> HTMLResponse:
except (ValueError, TypeError):
op_idx = 0
add_cod = str(form.get(f"chips_add_cod_{op_idx}") or "").strip().upper()
if add_cod and 0 <= op_idx < len(chips):
exists = conn.execute(
"SELECT 1 FROM nomenclator_rar WHERE cod_prestatie=?", (add_cod,)
).fetchone()
if exists:
if not add_cod:
chips_error = "Selecteaza un cod RAR din lista inainte de a adauga."
elif not (0 <= op_idx < len(chips)):
chips_error = "Operatia nu mai exista in formular — reincarca editarea."
elif not _cod_valid(add_cod):
chips_error = f"Cod necunoscut in nomenclator: {add_cod}."
else:
chips[op_idx]["cod_prestatie"] = add_cod
chips_ok = f"{add_cod} adaugat."
chips_added_cod = add_cod
elif action == "add_flat":
# Adauga cod nou in lista plata (fara op_service)
elif action in ("add_flat", "add_extra"):
# Adauga cod RAR liber (fara op_service): add_flat = mod plat,
# add_extra = mod operatii. Acelasi select (chips_add_cod_flat),
# aceeasi validare si acelasi dedup per-item (E4).
add_cod_flat = str(form.get("chips_add_cod_flat") or "").strip().upper()
if add_cod_flat:
exists = conn.execute(
"SELECT 1 FROM nomenclator_rar WHERE cod_prestatie=?", (add_cod_flat,)
).fetchone()
if exists:
chips.append({"cod_prestatie": add_cod_flat, "cod_op_service": "", "denumire": ""})
elif action == "add_extra":
# Adauga cod RAR liber (extra, fara op_service) in modul operatii.
# Refoloseste `chips_add_cod_flat` (acelasi select; dedup per-item pastrat).
# Select gol sau cod invalid → chips_extra_error = True (semnal vizibil).
add_cod_extra = str(form.get("chips_add_cod_flat") or "").strip().upper()
if add_cod_extra:
exists = conn.execute(
"SELECT 1 FROM nomenclator_rar WHERE cod_prestatie=?", (add_cod_extra,)
).fetchone()
if exists:
# Dedup per-item (E4): nu adauga un chip extra identic deja existent
if not add_cod_flat:
chips_error = "Selecteaza un cod RAR din lista inainte de a adauga."
elif not _cod_valid(add_cod_flat):
chips_error = f"Cod necunoscut in nomenclator: {add_cod_flat}."
else:
existing_pairs = {
(c.get("cod_op_service", ""), c.get("cod_prestatie", ""))
for c in chips
}
if ("", add_cod_extra) not in existing_pairs:
chips.append({"cod_prestatie": add_cod_extra, "cod_op_service": "", "denumire": ""})
if ("", add_cod_flat) in existing_pairs:
chips_error = f"{add_cod_flat} este deja in lista — nu a fost adaugat inca o data."
else:
chips_extra_error = True # cod necunoscut in nomenclator
else:
chips_extra_error = True # select gol
chips.append({"cod_prestatie": add_cod_flat, "cod_op_service": "", "denumire": ""})
chips_ok = f"{add_cod_flat} adaugat."
chips_added_cod = add_cod_flat
elif action == "remove":
# Sterge codul de la indexul dat (lasa op_service intact -> operatie ramane nemapata)
@@ -2480,7 +2486,9 @@ async def post_form_chips(request: Request) -> HTMLResponse:
"has_r_odo": has_r_odo,
"form_chips_url": "/form-chips",
"chips_section_id": "chips-section",
"chips_extra_error": chips_extra_error,
"chips_error": chips_error,
"chips_ok": chips_ok,
"chips_added_cod": chips_added_cod,
})

View File

@@ -46,6 +46,7 @@
{% if chip.cod_op_service %}
{% set _is_warn = chip.cod_prestatie in ('R-ODO', 'I-ODO') %}
{% set _nemapat = not chip.cod_prestatie %}
{% set _e_nou = (chips_added_cod is defined) and chips_added_cod and chip.cod_prestatie == chips_added_cod %}
<div class="op-row {% if _nemapat %}op-row-warn{% endif %}" style="margin-bottom:6px;">
<span class="op-row-name">
{{ chip.cod_op_service }}
@@ -59,11 +60,12 @@
<span style="display:flex;align-items:center;gap:8px;">
{% if chip.cod_prestatie %}
{# ===== Operatie mapata: chip cu × ===== #}
<span class="chip {% if _is_warn %}chip-warn{% endif %}"
<span class="chip {% if _is_warn %}chip-warn{% endif %} {% if _e_nou %}chip-nou{% endif %}"
aria-label="Prestatie {{ chip.cod_prestatie }} adaugata pentru {{ chip.cod_op_service }}">
{{ chip.cod_prestatie }}
<button type="button" class="chip-del"
hx-post="{{ _chips_url }}"
hx-disabled-elt="this"
hx-include="closest form"
hx-target="#{{ _sec_id }}"
hx-swap="outerHTML"
@@ -81,6 +83,7 @@
<button type="button"
style="font-size:10px;color:var(--muted);background:none;border:none;cursor:pointer;text-decoration:underline;padding:0;margin-left:4px;line-height:1;"
hx-post="/trimitere/{{ _sub_id }}/salveaza-regula-chip"
hx-disabled-elt="this"
hx-include="closest form"
hx-target="#detaliu-modal-body"
hx-swap="innerHTML"
@@ -104,6 +107,7 @@
<button type="button"
class="add-code"
hx-post="{{ _chips_url }}"
hx-disabled-elt="this"
hx-include="closest form"
hx-target="#{{ _sec_id }}"
hx-swap="outerHTML"
@@ -126,11 +130,13 @@
<div class="chips" role="group" aria-label="Coduri RAR suplimentare" style="margin-top:4px;">
{% for chip in _extra_chips %}
{% set _is_warn_extra = chip.cod_prestatie in ('R-ODO', 'I-ODO') %}
<span class="chip {% if _is_warn_extra %}chip-warn{% endif %}"
{% set _e_nou_extra = (chips_added_cod is defined) and chips_added_cod and chip.cod_prestatie == chips_added_cod %}
<span class="chip {% if _is_warn_extra %}chip-warn{% endif %} {% if _e_nou_extra %}chip-nou{% endif %}"
aria-label="Cod RAR suplimentar {{ chip.cod_prestatie }}">
{{ chip.cod_prestatie }}
<button type="button" class="chip-del"
hx-post="{{ _chips_url }}"
hx-disabled-elt="this"
hx-include="closest form"
hx-target="#{{ _sec_id }}"
hx-swap="outerHTML"
@@ -153,6 +159,7 @@
<button type="button"
class="add-code"
hx-post="{{ _chips_url }}"
hx-disabled-elt="this"
hx-include="closest form"
hx-target="#{{ _sec_id }}"
hx-swap="outerHTML"
@@ -167,24 +174,19 @@
Nomenclator indisponibil — adaugarea de coduri suplimentare nu e posibila.
</div>
{% endif %}
{% if chips_extra_error %}
{# T-C1/T-E4 (5.16): semnal vizibil cand add_extra are select gol sau cod invalid #}
<div class="chips-extra-error" style="font-size:11px;color:var(--err);padding:2px 0;" role="alert">
Selecteaza un cod RAR din lista inainte de a adauga.
</div>
{% endif %}
{% else %}
{# ===== Mod plat: lista de coduri libere (corectie pura, fara op_service) ===== #}
<div class="chips" role="group" aria-label="Coduri RAR selectate">
{% for chip in _chips %}
{% if chip.cod_prestatie %}
{% set _is_warn_flat = chip.cod_prestatie in ('R-ODO', 'I-ODO') %}
<span class="chip {% if _is_warn_flat %}chip-warn{% endif %}"
{% set _e_nou_flat = (chips_added_cod is defined) and chips_added_cod and chip.cod_prestatie == chips_added_cod %}
<span class="chip {% if _is_warn_flat %}chip-warn{% endif %} {% if _e_nou_flat %}chip-nou{% endif %}"
aria-label="Prestatie {{ chip.cod_prestatie }}">
{{ chip.cod_prestatie }}
<button type="button" class="chip-del"
hx-post="{{ _chips_url }}"
hx-disabled-elt="this"
hx-include="closest form"
hx-target="#{{ _sec_id }}"
hx-swap="outerHTML"
@@ -207,6 +209,7 @@
<button type="button"
class="add-code"
hx-post="{{ _chips_url }}"
hx-disabled-elt="this"
hx-include="closest form"
hx-target="#{{ _sec_id }}"
hx-swap="outerHTML"
@@ -224,6 +227,18 @@
</div>
{% endif %}
{# Rezultatul ultimei adaugari, in ambele moduri: refuz (rol=alert) sau confirmare.
Clasa chips-extra-error pastrata pe eroare (selector stabil pentru teste/QA). #}
{% if chips_error is defined and chips_error %}
<div class="chips-extra-error" style="font-size:11px;color:var(--err);padding:2px 0;" role="alert">
{{ chips_error }}
</div>
{% elif chips_ok is defined and chips_ok %}
<div class="chips-adaugat-ok" style="font-size:11px;color:var(--ok);padding:2px 0;" role="status">
{{ chips_ok }}
</div>
{% endif %}
{# Hint discret fara chips (debut) #}
{% if not _chips %}
<div style="font-size:10px;color:var(--muted);padding:4px 0;">

View File

@@ -178,18 +178,19 @@
<!-- ============================================================ -->
<div class="card">
<h2 style="font-size:15px; margin:0 0 8px;">Reguli automate (text)</h2>
<p class="muted" style="margin:0 0 12px; font-size:13px; max-width:680px;">
O regula leaga orice operatie al carei text <strong>contine</strong> (nu egal, ci substring)
un cuvant de un cod RAR. Util pentru operatii fara cod intern: ex. orice operatie care
<em>contine</em> „verificare" primeste codul ales. Match insensibil la majuscule/diacritice.
</p>
{% if not text_rules %}
<div class="empty" style="margin-bottom:12px;">
Inca nu ai reguli. Ex: operatia contine «verificare» &rarr; OE-2.
Mapeaza automat operatii similare fara cod intern. Adauga prima regula mai jos.
{# Un exemplu concret in loc de descriere — arata direct formatul unei reguli. #}
<div class="regula-exemplu"
style="margin:0 0 12px; font-size:13.5px; max-width:680px; padding:10px 12px;
border:1px dashed var(--line); border-radius:8px; background:var(--card2);">
Daca operatia contine
<span style="font-family:var(--font-mono); font-size:12.5px; padding:1px 6px; border-radius:4px;
background:color-mix(in srgb, var(--warn) 18%, transparent); color:var(--warn);">verificare</span>
<span class="muted" aria-hidden="true">&rarr;</span>
Cod RAR =
<span style="font-family:var(--font-mono); font-size:12.5px; font-weight:700; padding:1px 6px; border-radius:4px;
background:color-mix(in srgb, var(--accent) 16%, transparent); color:var(--accent);">OE-2</span>
Intretinere si revizie
</div>
{% endif %}
<div class="tablewrap tabel-card">
<table>

View File

@@ -60,45 +60,70 @@
hx-disinherit="hx-confirm"
style="margin:0;">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<div style="display:flex; justify-content:flex-end; align-items:center;
gap:6px; margin-bottom:8px; flex-wrap:wrap;">
{# Bulk-fix: input cod + buton aplica #}
{# Bara contextuala bulk (5.22): randata inactiva (ascunsa prin CSS), afisata de
JS (base.html) cand >= 1 checkbox e bifat. Contoarele .bulk-n / .bulk-n-txt
sunt actualizate client-side. Endpoint-urile si CSRF raman neschimbate;
"Sterge" pastreaza hx-confirm-ul form-ului, "Aplica" e exclus (hx-disinherit). #}
<div id="bulk-bar" class="bulk-bar" role="toolbar" aria-label="Actiuni pe randurile selectate">
<span style="font-weight:700;"><span class="bulk-n">0</span> selectate</span>
<span style="color:var(--muted);" aria-hidden="true">·</span>
<input type="text" name="cod_prestatie" id="bulk-fix-cod"
placeholder="Cod RAR (ex: OE-1)"
autocomplete="off" autocapitalize="characters"
style="width:120px; font-size:12px; padding:3px 7px;
border:1px solid var(--line); border-radius:5px;
background:var(--card2); color:var(--ink);"
background:var(--card2); color:var(--ink); font-family:var(--font-mono);"
aria-label="Cod RAR de aplicat la randurile selectate">
<button type="button"
hx-post="/trimiteri/bulk-fix"
hx-target="#submissions-wrap"
hx-swap="innerHTML"
style="background:var(--card); color:var(--accent); border-color:var(--accent);
font-size:13px; padding:4px 10px; border-radius:5px; cursor:pointer;"
style="background:var(--card); color:var(--accent); border:1px solid var(--accent);
font-size:12.5px; padding:4px 10px; border-radius:5px; cursor:pointer;"
aria-label="Aplica codul RAR la randurile blocate selectate">
Aplica cod
Aplica acest cod la <span class="bulk-n">0</span> <span class="bulk-n-txt">randuri</span>
</button>
{# Separator vizual #}
<span style="color:var(--muted); font-size:11px; padding:0 2px;" aria-hidden="true">|</span>
{# Bulk-delete: pastreaza exact comportamentul existent #}
<button type="submit" id="bulk-sterge-btn"
style="background:var(--card); color:var(--err); border-color:var(--err);
font-size:13px; padding:4px 10px; border-radius:5px; cursor:pointer;">
Sterge selectate
style="background:var(--card); color:var(--err); border:1px solid var(--err);
font-size:12.5px; padding:4px 10px; border-radius:5px; cursor:pointer;">
Sterge <span class="bulk-n">0</span> <span class="bulk-n-txt">randuri</span>
</button>
<button type="button" id="bulk-deselect"
style="margin-left:auto; background:none; border:none; color:var(--muted);
cursor:pointer; text-decoration:underline; font-size:12px;">
Deselecteaza
</button>
</div>
<div class="bulk-hint">
Aplica codul RAR doar operatiilor fara cod din randurile selectate, apoi le repune in coada.
</div>
{# Lista slim trimiteri.
Inlocuieste tabelul cu randuri compacte: VIN mono + operatie·ora + pill.
Nr. inmatriculare, data prestatie si nr. prezentare RAR raman accesibile
pe linia meta discreta (linia 3) si in modalul de detaliu. #}
{# Tabel compact trimiteri (grid CSS, PRD 5.22).
Desktop: un rand per trimitere sub un header de coloane; status = bulina cu
tooltip, mediu = litera T/P, multi-cod = primul cod + badge +N.
Mobil (max-width:767px): header ascuns, randul revine la 2 linii
(placuta / cod·operatie·data) cu litera + bulina in dreapta. #}
<ul class="lista-trimiteri-slim" role="list"
aria-label="Lista trimiteri">
{# Header de coloane — doar desktop (CSS); numele starii/mediului stau aici,
celulele raman semnale compacte (bulina, litera). #}
<li class="trimiteri-head" aria-hidden="true">
<span></span>
<span>Vehicul</span>
<span>Cod RAR</span>
<span>Operatie</span>
<span>Data</span>
<span class="th-c">Status</span>
<span class="th-c">Mediu</span>
<span></span>
</li>
{% for r in rows %}
{# Randul slim: stanga = VIN mono scurt (L1) + operatie·ora muted (L2) + meta (L3);
dreapta = pill de stare. Click deschide modalul global (#detaliu-modal-body).
Clickabil/focusabil (role=button); Enter/Space deschid modalul (JS in base.html). #}
{# Rand clickabil (role=button, Enter/Space in JS din base.html) — deschide
modalul global de detaliu (#detaliu-modal-body). #}
<li id="trimitere-row-{{ r.id }}"
class="trimitere-slim"
data-detaliu-id="{{ r.id }}"
@@ -110,24 +135,17 @@
style="cursor:pointer;"
title="Click pentru detaliul complet">
{# Zona checkbox — nu declanseaza modalul (stopPropagation).
Vizibila DOAR pe randurile gestionabile (error/needs_data/needs_mapping).
Latimea fixa previne reflow la prezenta/absenta checkbox-ului. #}
<div style="flex:0 0 22px; display:flex; align-items:center;" onclick="event.stopPropagation();">
{# Checkbox — nu declanseaza modalul (stopPropagation). Vizibil DOAR pe
randurile gestionabile; latimea fixa previne reflow. #}
<div class="c-check" onclick="event.stopPropagation();">
{% if r.gestionabil %}
<input type="checkbox" name="submission_id" value="{{ r.id }}"
aria-label="Selecteaza trimiterea #{{ r.id }} pentru stergere">
{% endif %}
</div>
{# Bloc text principal — stanga, ocupa spatiul ramas. Rand de 2 linii (spec 5.16):
L1 = placuta (identificator primar); L2 = cod RAR · operatie · data prestatie. #}
<div style="flex:1 1 auto; min-width:0;">
{# Linia 1: nr. inmatriculare (placuta) — identificatorul primar pe care il
scaneaza operatorul. .slim-vin reumplut (acelasi nume de clasa, churn minim).
Fallback cand placuta lipseste ('—'): VIN scurt, apoi mesaj neutru
(nu randa em-dash izolat ca identificator). #}
{# Vehicul: placuta (identificator primar); fallback VIN scurt / 'fara numar'
(niciodata em-dash izolat). #}
{% if r.prez.vehicul_nr and r.prez.vehicul_nr != '—' %}
<div class="slim-vin">{{ r.prez.vehicul_nr }}</div>
{% elif r.prez.vin_scurt and r.prez.vin_scurt != '—' %}
@@ -136,40 +154,42 @@
<div class="slim-vin muted">fara numar</div>
{% endif %}
{# Linia 2: cod RAR (sau 'nemapat') · operatie (ink, ellipsis) · data prestatie.
Separatorul "·" e injectat prin CSS intre celule. Operatia primeste ellipsis
ca randul sa NU treaca pe a 3-a linie nici la 390px.
VIN integral, #id_prezentare si secundele traiesc in modalul de detaliu. #}
<div class="slim-meta slim-rand2">
{% if r.prez.cod_rar and r.prez.cod_rar != '—' %}
<span class="cod-rar-cod">{{ r.prez.cod_rar }}</span>
{# Pe desktop .c-meta e display:contents (copiii devin celule de grid);
pe mobil e linia 2 (flex) cu separatoare "·" din CSS. #}
<div class="c-meta">
{% set _cods = r.prez.coduri_rar or [] %}
<span class="c-cod">
{% if _cods %}
<span class="cod-rar-cod">{{ _cods[0] }}</span>
{%- if _cods | length > 1 -%}
<span class="cod-plus" title="{{ _cods | join(' · ') }}">+{{ _cods | length - 1 }}</span>
{%- endif %}
{% else %}
<span class="cod-rar-cod cod-rar-sub muted">nemapat</span>
{% endif %}
<span class="slim-op">{{ r.prez.operatie }}</span>
{% if r.prez.data_prestatie and r.prez.data_prestatie != '—' %}
<span class="slim-data muted">{{ r.prez.data_prestatie }}</span>
{% endif %}
</span>
<span class="c-op">{{ r.prez.operatie }}</span>
<span class="c-data">{% if r.prez.data_prestatie and r.prez.data_prestatie != '—' %}{{ r.prez.data_prestatie }}{% endif %}</span>
</div>
{# Micro-linie umana a problemei — text mic s-error, DOAR pe stari de problema
(loud-on-exception D6). Randul normal/finalizat ramane strict 2 linii.
Token tipografic --fs-xs (>=12px, scala 5.16). #}
{% if r.eticheta_problema and r.eticheta_problema != r.stare_scurt and r.eticheta_problema != r.stare_text %}
<div class="eticheta-problema s-error">{{ r.eticheta_problema }}</div>
{% endif %}
</div>
{# Status: doar bulina colorata; numele complet in title + aria-label. #}
<span class="c-status">
<span class="dot-stare {{ r.stare_css }}" title="{{ r.stare_text }}"
role="img" aria-label="{{ r.stare_text }}"></span>
</span>
{# Zona dreapta: pill stare + badge mediu RAR + buton Trimite #}
<div style="flex:0 0 auto; display:flex; flex-direction:column; align-items:flex-end; gap:3px; white-space:nowrap;">
<span class="pill {{ r.stare_css }}" title="{{ r.stare_text }}">{{ r.stare_scurt }}</span>
{# Mediu: litera compacta — P fill atentie (productie, risc L.142),
T contur discret (testare). Numele complet in title. #}
<span class="c-env">
{% if r.rar_env %}
{% set _eb = eticheta_env(r.rar_env) %}
<span class="{{ _eb[1] }}">{{ _eb[0] }}</span>
<span class="env-l {{ 'env-l-prod' if 'prod' in _eb[1] else 'env-l-test' }}"
title="{{ _eb[0] }}" role="img" aria-label="Mediu RAR: {{ _eb[0] }}">{{ _eb[0][0] }}</span>
{% endif %}
{# Buton "Trimite" DOAR pe randurile tinute (queued AND held=1).
Afordanta dedicata cu stopPropagation (randul e role=button) — NU declanseaza modalul
de detaliu. Confirmare 1-linie prin hx-confirm (per rand); ireversibilitate in microcopy. #}
</span>
{# Actiuni: buton "Trimite" DOAR pe randurile tinute (queued AND held=1). #}
<div class="c-act">
{% if r.tinut %}
<form hx-post="/trimitere/{{ r.id }}/trimite-acum"
hx-target="#submissions-wrap" hx-swap="innerHTML"
@@ -180,7 +200,7 @@
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<button type="submit" class="btn-trimite-rand"
onclick="event.stopPropagation();"
style="margin-top:2px; padding:2px 10px; font-size:var(--fs-xs);
style="padding:2px 10px; font-size:var(--fs-xs);
border:1px solid var(--accent); border-radius:99px; cursor:pointer;
background:var(--accent); color:#fff; font-weight:600;">
Trimite
@@ -188,6 +208,12 @@
</form>
{% endif %}
</div>
{# Sub-linie de eroare: textul BRUT (mesaj RAR/validare), compact, un rand,
doar pe error/needs_data (eroare_bruta e "" in rest). #}
{% if r.eroare_bruta %}
<div class="c-err" title="{{ r.eroare_bruta }}">{{ r.eroare_bruta }}</div>
{% endif %}
</li>
{% endfor %}
</ul>

View File

@@ -741,35 +741,68 @@
.compact-nr { font-size:var(--fs-xl); font-weight:700; line-height:1; }
.compact-lbl { font-size:10px; color:var(--muted); margin-top:3px; white-space:nowrap; }
}
/* .lista-trimiteri-slim + .trimitere-slim — lista compacta cu separator --line2.
Randul e clickabil (rol button), tinta min-height:44px pe mobil. */
/* Bara contextuala bulk (5.22): ascunsa fara selectie; .activ (setat de JS
cand >=1 checkbox e bifat) o afiseaza impreuna cu hint-ul de sub ea. */
.bulk-bar { display:none; align-items:center; gap:8px; flex-wrap:wrap; font-size:13px;
background:color-mix(in srgb, var(--accent) 10%, var(--card));
border:1px solid color-mix(in srgb, var(--accent) 40%, transparent);
border-radius:8px; padding:8px 12px; margin-bottom:4px; }
.bulk-bar.activ { display:flex; }
.bulk-hint { display:none; font-size:12px; color:var(--muted); margin:0 0 8px; }
.bulk-bar.activ + .bulk-hint { display:block; }
/* Tabel compact trimiteri (grid CSS, 5.22).
Desktop: header de coloane + un rand per trimitere; sub-linia de eroare
(.c-err) ocupa coloanele 2..-1 pe un al doilea rand de grid.
Mobil (max-width:767px, mai jos): header ascuns, rand pe 2 linii. */
.lista-trimiteri-slim { list-style:none; margin:0; padding:0; }
.trimitere-slim { display:flex; align-items:center; justify-content:space-between; gap:12px;
padding:14px 16px; border-bottom:1px solid var(--line2); min-height:44px; cursor:pointer; }
.trimiteri-head, .trimitere-slim {
display:grid; align-items:center; column-gap:10px; row-gap:2px;
grid-template-columns:22px minmax(90px,150px) minmax(64px,auto) minmax(0,1fr) auto 46px 44px 84px; }
.trimiteri-head { padding:2px 16px 8px; border-bottom:1px solid var(--line);
font-size:10.5px; font-weight:700; letter-spacing:.05em; text-transform:uppercase;
color:var(--muted); }
.trimiteri-head .th-c { text-align:center; }
.trimitere-slim { padding:9px 16px; border-bottom:1px solid var(--line2);
min-height:44px; cursor:pointer; }
.trimitere-slim:last-child { border-bottom:none; }
.trimitere-slim:hover { background:color-mix(in srgb, var(--accent) 6%, transparent); }
.trimitere-slim:focus, .trimitere-slim:focus-visible { outline:2px solid var(--accent); outline-offset:-2px; }
.slim-vin { font-family:var(--font-mono); font-size:var(--fs-md); font-weight:600; color:var(--ink); }
.slim-meta { font-size:var(--fs-sm); color:var(--muted); margin-top:3px; }
/* Linia 2 a randului slim (5.16): cod RAR · operatie (ellipsis) · data, pe UN rand.
Ellipsis-ul pe operatie garanteaza 2 linii MAX si la 390px. */
.slim-rand2 { display:flex; align-items:baseline; gap:6px; min-width:0; }
.slim-rand2 .cod-rar-cod { flex:0 0 auto; font-family:var(--font-mono); font-weight:600;
color:var(--accent); }
.slim-rand2 .cod-rar-cod.muted { color:var(--muted); font-weight:500; }
.slim-rand2 .slim-op { flex:1 1 auto; min-width:0; white-space:nowrap; overflow:hidden;
text-overflow:ellipsis; color:var(--ink); }
.slim-rand2 .slim-data { flex:0 0 auto; }
.slim-rand2 .slim-op::before, .slim-rand2 .slim-data::before {
content:"·"; color:var(--muted); margin-right:6px; }
.lista-trimiteri-slim .eticheta-problema { font-size:var(--fs-xs); line-height:1.3; margin-top:2px; }
/* Pill slim (5.16): fill-tint + dot 7px + text colorat per stare (currentColor din .s-*).
Pastrat pe FIECARE rand inclusiv Finalizat (linistit dar prezent). */
.lista-trimiteri-slim .pill { display:inline-flex; align-items:center; gap:5px; font-weight:600;
background:color-mix(in srgb, currentColor 14%, transparent);
border-color:color-mix(in srgb, currentColor 35%, transparent); }
.lista-trimiteri-slim .pill::before { content:""; width:7px; height:7px; border-radius:99px;
background:currentColor; flex-shrink:0; }
.trimitere-slim .c-check { display:flex; align-items:center; }
.slim-vin { font-family:var(--font-mono); font-size:var(--fs-sm); font-weight:600; color:var(--ink);
white-space:nowrap; overflow:hidden; text-overflow:ellipsis; min-width:0; }
/* Pe desktop copiii .c-meta devin celule de grid (cod / operatie / data). */
.trimitere-slim .c-meta { display:contents; }
.trimitere-slim .c-cod { white-space:nowrap; }
.trimitere-slim .cod-rar-cod { font-family:var(--font-mono); font-weight:600;
font-size:var(--fs-xs); color:var(--accent); }
.trimitere-slim .cod-rar-cod.muted { color:var(--muted); font-weight:500; font-style:italic; }
/* Badge +N: trimiterea are mai multe coduri RAR; toate stau in title. */
.trimitere-slim .cod-plus { display:inline-block; font-family:var(--font-mono);
font-size:10px; font-weight:700; color:var(--muted); border:1px solid var(--line);
border-radius:99px; padding:0 5px; margin-left:4px; vertical-align:1px; cursor:default; }
.trimitere-slim .c-op { min-width:0; white-space:nowrap; overflow:hidden;
text-overflow:ellipsis; font-size:var(--fs-sm); color:var(--ink); }
.trimitere-slim .c-data { font-size:var(--fs-xs); color:var(--muted); white-space:nowrap;
font-variant-numeric:tabular-nums; }
.trimitere-slim .c-status, .trimitere-slim .c-env { justify-self:center; line-height:1;
display:inline-flex; align-items:center; }
/* Bulina de status: doar cerc colorat (currentColor din .s-*); numele starii
traieste in headerul coloanei + title/aria-label. */
.dot-stare { display:inline-block; width:11px; height:11px; border-radius:99px;
background:currentColor; }
/* Litera de mediu: P = fill atentie (declaratie reala, ireversibila — L.142);
T = contur discret (mediu de proba). */
.env-l { display:inline-flex; align-items:center; justify-content:center;
width:20px; height:20px; border-radius:99px; font-size:11px; font-weight:800;
cursor:default; }
.env-l-prod { background:color-mix(in srgb, var(--err) 80%, var(--card)); color:var(--card); }
.env-l-test { color:var(--muted); border:1px solid var(--line); }
.trimitere-slim .c-act { justify-self:end; }
/* Sub-linia de eroare: text brut, mono, un singur rand cu ellipsis (title = complet). */
.trimitere-slim .c-err { grid-column:2 / -1; min-width:0; font-family:var(--font-mono);
font-size:var(--fs-xs); color:var(--err); white-space:nowrap; overflow:hidden;
text-overflow:ellipsis; }
/* .camp-slim — varianta compacta camp formular: label --fs-sm muted deasupra, input --fs-md, fundal --card2.
Mono pentru campuri VIN/odometru/nr: adauga clasa .camp-mono pe input. */
.camp-slim { margin-bottom:8px; }
@@ -793,6 +826,8 @@
.chip .chip-del:focus-visible { outline:2px solid var(--accent); outline-offset:1px; }
/* Varianta chip warn (ex. R-ODO necesita odometruInitial) */
.chip-warn { background:color-mix(in srgb, var(--warn) 22%, transparent); color:var(--warn); }
/* Chip abia adaugat via /form-chips — confirmare vizibila a actiunii "+" */
.chip-nou { outline:2px solid color-mix(in srgb, var(--ok) 45%, transparent); outline-offset:1px; }
/* .add-code — buton dashed pentru adaugare cod in chipbox */
.add-code { display:inline-flex; align-items:center; height:22px; padding:0 7px; background:transparent;
border:1px dashed color-mix(in srgb, var(--accent) 55%, var(--line));
@@ -806,9 +841,30 @@
vizuala a randului de mapare op<->cod. */
.op-row-name { font-size:var(--fs-sm); font-weight:700; color:var(--ink); }
.op-row-warn { border-color:color-mix(in srgb, var(--warn) 45%, var(--line)); }
/* Mobil: tinta touch pentru trimitere-slim (deja garantata prin min-height:44px in regula de baza) */
/* Mobil: fara header, randul revine la 2 linii (placuta / cod·operatie·data),
cu actiune + litera mediu + bulina status in dreapta. Grid-areas peste
aceleasi celule; .c-meta redevine linie flex cu separatoare "·". */
@media (max-width:767px) {
.trimitere-slim { padding:12px 14px; }
.trimiteri-head { display:none; }
.trimitere-slim { padding:12px 14px; column-gap:8px;
grid-template-columns:22px minmax(0,1fr) auto auto auto;
grid-template-areas:
"check veh act env status"
"check meta act env status"
"err err err err err"; }
.trimitere-slim .c-check { grid-area:check; }
.trimitere-slim .slim-vin { grid-area:veh; font-size:var(--fs-md); }
.trimitere-slim .c-meta { grid-area:meta; display:flex; align-items:baseline;
gap:6px; min-width:0; margin-top:3px; }
.trimitere-slim .c-cod { flex:0 0 auto; }
.trimitere-slim .c-op { flex:1 1 auto; }
.trimitere-slim .c-data { flex:0 0 auto; }
.trimitere-slim .c-op::before, .trimitere-slim .c-data:not(:empty)::before {
content:"·"; color:var(--muted); margin-right:6px; }
.trimitere-slim .c-status { grid-area:status; }
.trimitere-slim .c-env { grid-area:env; }
.trimitere-slim .c-act { grid-area:act; }
.trimitere-slim .c-err { grid-area:err; }
}
/* === SENTINEL-COMPONENTE-SLIM: sfarsit componente slim === */
/* === Fix mobil Mapari ===
@@ -1222,6 +1278,34 @@
if (saveBtn) saveBtn.classList.add('dirty');
});
</script>
<script>
// Bara contextuala bulk (trimiteri): vizibila DOAR cu selectie.
// Delegare pe document (supravietuieste swap-urilor HTMX); dupa un swap al
// listei checkbox-urile se reseteaza, deci sync() ascunde bara automat.
(function() {
function syncBulkBar() {
var form = document.getElementById('bulk-trimiteri');
var bar = document.getElementById('bulk-bar');
if (!form || !bar) return;
var n = form.querySelectorAll('input[name="submission_id"]:checked').length;
bar.classList.toggle('activ', n > 0);
bar.querySelectorAll('.bulk-n').forEach(function(el) { el.textContent = n; });
bar.querySelectorAll('.bulk-n-txt').forEach(function(el) {
el.textContent = (n === 1 ? 'rand' : 'randuri');
});
}
document.addEventListener('change', function(e) {
if (e.target && e.target.name === 'submission_id') syncBulkBar();
});
document.addEventListener('click', function(e) {
if (!(e.target.closest && e.target.closest('#bulk-deselect'))) return;
document.querySelectorAll('#bulk-trimiteri input[name="submission_id"]:checked')
.forEach(function(cb) { cb.checked = false; });
syncBulkBar();
});
document.body.addEventListener('htmx:afterSwap', syncBulkBar);
})();
</script>
<script>
// Sub-taburi pagina Setari: comutare pur client-side, fara server.
// Delegare pe document -> supravietuieste swap-urilor HTMX ale #card-cont (dupa

View File

@@ -48,7 +48,9 @@ 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-07-06**5.21 CACHE PERSISTENT EMBEDDINGS IN SQLITE — LIVRAT** (PRD: [prd-5.21](prd/prd-5.21-embedding-cache-sqlite.md); executie echipa de agenti Sonnet: cache-core T1+T2, wiring T3+T5, tests T4+T6, reviewer adversarial). Vectorii corpusului k-NN (17.181 exemple SILVER) persista in tabela noua `embedding_cache` (PK `(model, text_hash)`, blob float32 LE 384, `array('f')` end-to-end); la warmup se vectorizeaza DOAR miss-urile (hash sha256 pe `denumire_normalizata` din lista FILTRATA, identica cu ce intra in `index_corpus`), restarturile cu corpus neschimbat = `embed=0`. Modul nou `app/embedding_cache.py` (fara dependinte noi): `sync_corpus_vectors(conn, model, texts, embed_fn)` hash->load->embed(miss)->save; scrieri/stergeri in chunk-uri de 500 cu `BEGIN`/`COMMIT` EXPLICIT (conexiunile sunt autocommit — `conn.commit()` e no-op); validare dimensiune la scriere SI citire (blob corupt = miss re-vectorizat); purjare orfane + modele vechi DOAR dupa indexare reusita; toate erorile SQLite -> `log.warning` + fallback embed complet (degradare gratioasa, nimic propagat). `EmbeddingEngine.index_corpus(vectors=)` valideaza alinierea `len(vectors)==len(items)` + absenta None (mismatch -> fallback embed complet). Secventa protejata de `_embeddings_lock` (mapping.py); pe calea de request (`block=False`) acquire NE-blocant — daca warmup-ul tine lock-ul, return imediat (fix MAJOR din review: altfel primul request in fereastra de cold start ingheta ~1-2 min). Log observabilitate: `embeddings: warmup ok cache=N embed=M in Xs` (eticheta `corpus reindexat` pe calea de request). Semnatura corpusului (`_corpus_signature_silver`) = mecanism de decizie NESCHIMBAT; worker-ul nu atinge cache-ul. Review adversarial: 1 MAJOR + 2 MINOR + 1 NIT, toate reparate. Suita completa: **1596 passed, 1 skipped** (+31 teste noi: 15/15 cai din diagrama de acoperire, toate cu `AUTOPASS_EMBEDDINGS_ENABLED=1` explicit, backend mock). Crestere DB ~26-30MB acceptata; deferate in TODOS.md: CLI embcache, precompute la build, graceful reload, numpy la >50k randuri. **De verificat pe instanta reala (US-006)**: doua restarturi `./start.sh`, al doilea cu `embed=0` si sugestia "similaritate" in <30s.
**Ultima actualizare**: 2026-07-07**5.22 UI COMPACT TRIMITERI + FIX BUTON "+" EDITOR — LIVRAT** (PRD: [prd-5.22](prd/prd-5.22-ui-compact-trimiteri-reguli-fix-chips.md); mockup aprobat de utilizator inainte de executie). Trimiteri pe desktop = tabel grid cu header (Vehicul/Cod RAR/Operatie/Data/Status/Mediu), un rand per trimitere: status = bulina colorata cu tooltip (`.dot-stare`), mediu = litera `P` (fill --err, risc L.142) / `T` (contur), multi-cod = primul cod + badge `+N` cu toate codurile in title (`payload_view.coduri_rar` nou), sub-linie de eroare = textul BRUT (mesaj RAR, ex. ORA-12899) mono trunchiat doar pe error/needs_data (`labels.eroare_bruta` nou; needs_mapping exclus — "nemapat" e in coloana Cod RAR). Mobil (<=767px): fara header, rand pe 2 linii prin grid-areas, T/P + bulina in dreapta. Actiunile bulk ("Aplica cod"/"Sterge") mutate intr-o **bara contextuala** vizibila doar la selectie, cu contor si texte explicite ("Aplica acest cod la N randuri") + hint; JS delegat + sync la `htmx:afterSwap`. Mapari: descrierea sectiunii "Reguli automate (text)" inlocuita cu un exemplu concret formatat. **Fix bug "+" editor prestatii** — cauza-radacina gasita LA VERIFY E2E (nu in analiza statica): `hx-disabled-elt="find button"` de pe form-urile parinte se mosteneste pe butoanele chips → `find button` relativ la "+" = null → TypeError intern htmx 1.9.12 INAINTE de request (nicio cerere, nicio eroare) → fix `hx-disabled-elt="this"` explicit pe toate butoanele hx-post din `_chips_prestatii.html`; separat, no-op-urile silentioase server-side din `post_form_chips` (cod invalid pe add/add_flat, duplicat pe toate ramurile) au acum mesaje explicite (`chips_error`) si confirmare la succes (chip `.chip-nou` evidentiat + "X adaugat."). Verify E2E in browser pe desktop 1280 + mobil 390 (screenshots), bulk-fix aplicat live. Suita completa: **1622 passed, 1 skipped** (+26 teste noi). Observatii deferate: `bulk_message` e suprascris imediat de reload-ul `trimiteriChanged` (pre-existent); operatia trunchiata agresiv pe mobil pe randurile cu buton "Trimite".
> 2026-07-06 — **5.21 CACHE PERSISTENT EMBEDDINGS IN SQLITE — LIVRAT** (PRD: [prd-5.21](prd/prd-5.21-embedding-cache-sqlite.md); executie echipa de agenti Sonnet: cache-core T1+T2, wiring T3+T5, tests T4+T6, reviewer adversarial). Vectorii corpusului k-NN (17.181 exemple SILVER) persista in tabela noua `embedding_cache` (PK `(model, text_hash)`, blob float32 LE 384, `array('f')` end-to-end); la warmup se vectorizeaza DOAR miss-urile (hash sha256 pe `denumire_normalizata` din lista FILTRATA, identica cu ce intra in `index_corpus`), restarturile cu corpus neschimbat = `embed=0`. Modul nou `app/embedding_cache.py` (fara dependinte noi): `sync_corpus_vectors(conn, model, texts, embed_fn)` hash->load->embed(miss)->save; scrieri/stergeri in chunk-uri de 500 cu `BEGIN`/`COMMIT` EXPLICIT (conexiunile sunt autocommit — `conn.commit()` e no-op); validare dimensiune la scriere SI citire (blob corupt = miss re-vectorizat); purjare orfane + modele vechi DOAR dupa indexare reusita; toate erorile SQLite -> `log.warning` + fallback embed complet (degradare gratioasa, nimic propagat). `EmbeddingEngine.index_corpus(vectors=)` valideaza alinierea `len(vectors)==len(items)` + absenta None (mismatch -> fallback embed complet). Secventa protejata de `_embeddings_lock` (mapping.py); pe calea de request (`block=False`) acquire NE-blocant — daca warmup-ul tine lock-ul, return imediat (fix MAJOR din review: altfel primul request in fereastra de cold start ingheta ~1-2 min). Log observabilitate: `embeddings: warmup ok cache=N embed=M in Xs` (eticheta `corpus reindexat` pe calea de request). Semnatura corpusului (`_corpus_signature_silver`) = mecanism de decizie NESCHIMBAT; worker-ul nu atinge cache-ul. Review adversarial: 1 MAJOR + 2 MINOR + 1 NIT, toate reparate. Suita completa: **1596 passed, 1 skipped** (+31 teste noi: 15/15 cai din diagrama de acoperire, toate cu `AUTOPASS_EMBEDDINGS_ENABLED=1` explicit, backend mock). Crestere DB ~26-30MB acceptata; deferate in TODOS.md: CLI embcache, precompute la build, graceful reload, numpy la >50k randuri. **De verificat pe instanta reala (US-006)**: doua restarturi `./start.sh`, al doilea cu `embed=0` si sugestia "similaritate" in <30s.
> 2026-07-06 — **HARDENING 80/20 LIVRAT + PUSH** (`main` la `63b6cbc`; plan /autoplan `main-hardening-8020-plan-20260703.md`, audit securitate 2026-07-03, inainte de expunerea publica autopass.romfast.ro). **P0**: compose fail-safe — `:?` obligatoriu pe `AUTOPASS_REQUIRE_API_KEY`/`AUTOPASS_WORKER_SEND_ENABLED`/`AUTOPASS_RAR_ENV` (split-brain api-prod/worker-test eliminat; post-5.20 variabila e doar ancora de fallback, nu tinta trimiterilor), `FORWARDED_ALLOW_IPS="*"` ca rate-limit-ul sa vada IP-ul real dupa Traefik (invariante in comentariu: api fara `ports:` pe host, Traefik fara `forwardedHeaders.insecure`), `AUTOPASS_SESSION_SECRET` obligatoriu + `SESSION_HTTPS_ONLY` default true; invarianta E1 fail-fast la boot (`validate_prod_invariants`): prod fara cheie API sau session secret -> RuntimeError cu mesaj actionabil. **P0-4 backup criptat SQLite**: `tools/backup_db.sh` (snapshot online stdlib + gpg AES256 + retentie + rclone optional), `tools/restore_check.sh` (integrity_check pe restore), `docs/backup.md`; trigger dur: configurat INAINTE de prima declaratie reala prod. **P1**: headers de securitate in-app (`SecurityHeadersMiddleware`, HSTS doar pe https) + body-cap global 10MB (`BodyCapMiddleware` ASGI pur, 413 inainte de parserul multipart/JSON; verificarea per-endpoint ramane strat 2) + imagine Docker non-root (uid 10001, `HOME=/home/app` pt fastembed, chown /data INAINTE de VOLUME, loguri pe `/data/logs`, port 8010 aliniat EXPOSE/CMD; migrare one-off volum existent: `docker compose run --rm --user root api chown -R app:app /data`). **P2**: print-uri cu email in stdout eliminate (signup + notify degradat din `app/email.py` -> `log_event` fara PII) + fix crestere monotona chei `ratelimit._hits`. Executie multi-agent (4 agenti implementare pe fisiere disjuncte + review dedicat: 0 constatari blocante, 3 nit-uri acceptate). Suita completa: 1557 passed, 1 skipped (3 rulari independente). **Post-deploy HOUR-1 (Dokploy)**: seteaza env-urile obligatorii INAINTE de redeploy (`:?` pica pornirea), creeaza cheia API + tier pro/trial pe contul propriu (altfel 403 PLAN_FARA_API), ruleaza migrarea chown, seteaza passphrase-ul de backup.

View File

@@ -0,0 +1,209 @@
# PRD 5.22 — UI compact Trimiteri + bara contextuala "Aplica cod" + exemplu Reguli automate + fix buton "+" editor import
**Stare**: inchis
> Mockup aprobat de utilizator (artifact `6efc43e0`, v2, 2026-07-07). Decizii inchise:
> tabel desktop pe un rand cu header; status = bulina + tooltip; mediu = litera T/P;
> multi-cod = primul cod + badge `+N` cu tooltip; sub-linie de problema = textul brut
> al erorii, mono, trunchiat, doar pe erori; bara "Aplica cod" contextuala la selectie;
> Reguli automate = exemplu concret in loc de paragraf descriptiv; fix no-op silentios
> la butonul "+" din editorul de import.
## 1. Obiectiv
Patru imbunatatiri UI/UX aprobate pe mockup: (a) lista Trimiteri devine tabel compact
pe un singur rand pe desktop, cu header de coloane si semnalizare compacta
status/mediu; (b) actiunea bulk "Aplica cod" devine bara contextuala vizibila doar la
selectie; (c) sectiunea "Reguli automate (text)" din Mapari isi inlocuieste descrierea
cu un exemplu concret; (d) butonul "+" din editorul de prestatii (import) nu mai
esueaza silentios — feedback vizibil la succes si la refuz.
## 2. Non-Goals (anti scope-creep)
- NU se schimba logica backend a `bulk-fix` (`POST /trimiteri/bulk-fix`) — doar prezentarea.
- NU se schimba masina de stari `submissions`, filtrele, paginarea sau modalul de detaliu.
- NU se adauga functionalitate noua la regulile text (`/mapari/reguli-text`) — doar microcopy.
- NU se redeseneaza restul editorului de rand din import (doar zona chips "+").
- NU se ating API v1, worker-ul sau schema DB.
## 3. Stories atomice
### US-001: Fix buton "+" chips prestatii — feedback la refuz si la succes
**Ca** operator care editeaza un rand de import **vreau** ca apasarea pe "+" sa aiba
mereu un efect vizibil **pentru ca** azi codul invalid sau duplicat produce un raspuns
identic, fara niciun mesaj ("nu se intampla nimic").
- **Depinde de**: —
- **Fisiere**: `app/web/routes.py` (`post_form_chips`), `app/web/templates/_chips_prestatii.html`, `tests/test_web_corectie_prestatii.py` (~3 fisiere)
- **Test intai (RED)**: `tests/test_web_corectie_prestatii.py``test_add_cod_duplicat_afiseaza_mesaj`, `test_add_cod_invalid_afiseaza_mesaj`, `test_add_cod_reusit_marcheaza_chip_nou`
- **Acceptance criteria**:
- [ ] Ramurile `add`, `add_extra`, `add_flat` din `post_form_chips`: cod care nu trece
verificarea in `nomenclator_rar` → mesaj de eroare in fragmentul re-randat
("Cod necunoscut in nomenclator"), nu raspuns identic.
- [ ] Cod deja prezent in lista (dedup) → mesaj "X este deja in lista — nu a fost
adaugat inca o data", pe TOATE cele 3 ramuri.
- [ ] Adaugare reusita → chip-ul nou primeste clasa de evidentiere (`chip-nou`,
outline verde) + mesaj de confirmare "X adaugat" (aria-live pentru SR).
- [ ] Mesajele traiesc in fragmentul `_chips_prestatii.html` (swap `outerHTML` pe
`#chips-section`) si dispar la urmatoarea actiune reusita.
- [ ] `python3 -m pytest -q` verde.
- **Verificare E2E**: browser pe import → Editeaza rand → alege cod deja adaugat → "+"
→ mesaj rosu vizibil; alege cod nou → "+" → chip verde evidentiat + mesaj.
### US-002: Reguli automate — exemplu concret in loc de descriere
**Ca** utilizator al paginii Mapari **vreau** sa vad un exemplu de regula in loc de un
paragraf explicativ **pentru ca** exemplul arata direct formatul si scuteste cititul.
- **Depinde de**: —
- **Fisiere**: `app/web/templates/_mapari.html`, `tests/test_web_mapari_text_rules.py` (~2 fisiere)
- **Test intai (RED)**: `tests/test_web_mapari_text_rules.py``test_sectiune_reguli_afiseaza_exemplu_nu_descriere`
- **Acceptance criteria**:
- [ ] Textul descriptiv din capul sectiunii "Reguli automate (text)" (inclusiv
empty-state) e inlocuit cu un singur exemplu formatat:
`Daca operatia contine "verificare" → Cod RAR = OE-2 Intretinere si revizie`,
cu pattern-ul si codul evidentiate vizual (chip warn / chip accent, ca in mockup).
- [ ] Nicio schimbare functionala la tabel, adaugare, preview sau stergere reguli.
- [ ] `python3 -m pytest -q` verde.
- **Verificare E2E**: browser pe Mapari → sectiunea Reguli automate afiseaza exemplul.
### US-003: View-model rand trimitere — coduri multiple + eroare bruta
**Ca** dashboard **vreau** ca randul de trimitere sa expuna lista completa de coduri
RAR si textul brut al erorii **pentru ca** UI-ul compact afiseaza `primul cod +N`
(tooltip cu toate) si sub-linia de eroare cu mesajul RAR, nu eticheta explicativa.
- **Depinde de**: —
- **Fisiere**: `app/payload_view.py`, `app/web/routes.py` (`_submission_row_view`), `tests/test_payload_view.py`, `tests/test_web_submissions.py` (~4 fisiere)
- **Test intai (RED)**: `tests/test_payload_view.py``test_coduri_rar_lista_ordonata_dedup`, `test_coduri_rar_gol_fara_prestatii`; `tests/test_web_submissions.py``test_rand_expune_eroare_bruta_doar_pe_error`
- **Acceptance criteria**:
- [ ] `prezentare_din_payload` intoarce camp nou `coduri_rar`: lista ordonata,
dedup, a codurilor RAR (`cod_prestatie`) din TOATE prestatiile (lista goala
cand nu exista); campurile existente raman neschimbate (compatibilitate).
- [ ] `_submission_row_view` expune `eroare_bruta`: mesajul RAR brut cand exista
(`rar_error` → mesajul serverului, ex. `ORA-12899...`), altfel `motiv` scurt;
sir gol pe stari fara problema. `needs_mapping` NU produce `eroare_bruta`
(informatia "nemapat" traieste in coloana Cod RAR).
- [ ] `python3 -m pytest -q` verde.
- **Verificare E2E**: — (acoperit de US-004).
### US-004: Trimiteri desktop — tabel cu header, un rand per trimitere
**Ca** operator pe desktop **vreau** trimiterile intr-un tabel compact cu header de
coloane **pentru ca** este loc pe orizontala si scanez mai repede o lista pe un rand.
- **Depinde de**: US-003
- **Fisiere**: `app/web/templates/_submissions.html`, `app/web/templates/base.html` (CSS), `tests/test_web_submissions_layout.py` (~3 fisiere)
- **Test intai (RED)**: `tests/test_web_submissions_layout.py``test_desktop_header_coloane`, `test_status_bulina_cu_tooltip`, `test_mediu_litera_t_p`, `test_multicod_badge_plus_n`, `test_sublinie_eroare_bruta_doar_pe_error`
- **Acceptance criteria**:
- [ ] Pe desktop (>= breakpoint existent), lista devine tabel/grid cu header:
(checkbox) · Vehicul · Cod RAR · Operatie · Data · Status · Mediu · (actiuni).
Un rand per trimitere, fara wrap; operatia primeste ellipsis.
- [ ] Status = DOAR bulina colorata (culorile claselor `s-*` existente), nume stare
complet in `title` (tooltip); coloana are headerul "Status".
- [ ] Mediu = litera in coloana proprie cu header "Mediu": `P` alb pe fundal
`--err` (productie), `T` contur discret (testare); `title` cu numele complet.
- [ ] Mai multe coduri: primul cod + badge `+N` cu `title` care listeaza toate
codurile din `coduri_rar`; un singur cod → fara badge; niciun cod → `nemapat`.
- [ ] Sub-linia de problema apare DOAR cand `eroare_bruta` e ne-goala: text mono,
`--fs-xs`, culoare `--err`, un singur rand cu `text-overflow:ellipsis`,
`title` cu textul complet.
- [ ] Comportamente pastrate: click pe rand → modal detaliu; checkbox doar pe
`gestionabil`; buton "Trimite" doar pe `tinut`; paginare/filtre neatinse.
- [ ] `python3 -m pytest -q` verde.
- **Verificare E2E**: browser HTMX pe http://localhost:8010/ (desktop 1280px) —
randuri pe o singura linie, header vizibil, tooltip-uri corecte.
### US-005: Trimiteri mobil — bulina status + litera mediu
**Ca** operator pe mobil **vreau** aceleasi semnale compacte (bulina + T/P) pe lista
de 2 linii **pentru ca** pill-urile cu text ocupa spatiu inutil pe ecran mic.
- **Depinde de**: US-004
- **Fisiere**: `app/web/templates/_submissions.html`, `app/web/templates/base.html` (media query), `tests/test_web_submissions_layout.py` (~3 fisiere)
- **Test intai (RED)**: `tests/test_web_submissions_layout.py``test_mobil_pastreaza_doua_linii_cu_bulina_si_litera`
- **Acceptance criteria**:
- [ ] Sub breakpoint, randul revine la 2 linii (placuta / cod·operatie·data), iar in
dreapta stau litera de mediu + bulina de status (+ butonul "Trimite" pe tinute).
- [ ] Fara header de tabel pe mobil; fara scroll orizontal la 390px.
- [ ] `python3 -m pytest -q` verde.
- **Verificare E2E**: browser la 390px — lista lizibila, fara overflow orizontal.
### US-006: Bara contextuala "Aplica cod" — vizibila doar la selectie
**Ca** operator **vreau** ca actiunile bulk sa apara doar cand am selectat randuri,
cu text explicit **pentru ca** butonul permanent "Aplica cod" fara context e confuz.
- **Depinde de**: US-004
- **Fisiere**: `app/web/templates/_submissions.html`, `app/web/templates/base.html` (CSS + JS numarare selectie), `tests/test_web_bulk_fix.py` (~3 fisiere)
- **Test intai (RED)**: `tests/test_web_bulk_fix.py``test_bara_bulk_ascunsa_implicit`, `test_bara_bulk_texte_explicite`
- **Acceptance criteria**:
- [ ] Inputul de cod + "Aplica cod" + "Sterge selectate" dispar din capul listei;
apar intr-o bara contextuala (stil accent, ca in mockup) randata ascunsa si
afisata prin JS cand >= 1 checkbox e bifat; dispare la deselectare.
- [ ] Bara contine: contor "N selectate", input cod RAR, buton "Aplica acest cod la
N randuri", separator, buton "Sterge N randuri", actiune "Deselecteaza";
N se actualizeaza la fiecare bifare (JS, fara roundtrip).
- [ ] Sub bara: hint pe un rand — "Aplica codul RAR doar operatiilor fara cod din
randurile selectate, apoi le repune in coada."
- [ ] Endpoint-urile, CSRF, `hx-confirm` pe stergere si `hx-disinherit` raman
functional identice; `bulk_message` se afiseaza ca acum.
- [ ] Starea barei supravietuieste re-randarii fragmentului (re-evaluare la
`htmx:afterSwap` — dupa swap selectia se pierde, bara se ascunde).
- [ ] `python3 -m pytest -q` verde.
- **Verificare E2E**: browser — capul listei curat; bifez 2 randuri blocate → bara
apare cu "2 selectate"; aplic cod → lista se reincarca, bara dispare, sumar vizibil.
## 4. Riscuri
- **Tabel vs. lista pe acelasi fragment**: `_submissions.html` e folosit si de
paginare/filtre/OOB — orice schimbare de structura trebuie sa pastreze id-urile
(`#submissions-wrap`, `#bulk-trimiteri`, OOB-urile). Mitigare: pastram form-ul si
id-urile existente, schimbam doar interiorul listei; testele de layout existente
(`test_web_submissions_layout.py`) ruleaza inainte si dupa.
- **`rar_error` brut poate contine detalii tehnice lungi**: afisare pe un rand cu
ellipsis + `title`; nu se logheaza/expune nimic nou (textul exista deja in modal).
- **JS-ul de numarare selectie** ruleaza peste randuri re-randate de HTMX: legare prin
delegare pe container, nu pe noduri (altfel handler-ii mor la swap).
- **Doua semnale rotunde alaturate pe mobil** (bulina status + litera mediu): litera
are forma/continut diferit de bulina — validat in mockup; daca in practica se
confunda, fallback aprobat: mediu doar pe randurile de productie.
## 5. Intrebari deschise
— (toate inchise prin mockup v2 aprobat)
## 6. Valuri de executie (graful de dependente)
```
Val 1: [US-001] [US-002] [US-003] ← fara dependente, fisiere disjuncte → paralel
Val 2: [US-004] ← deblocat de US-003
Val 3: [US-005] [US-006] ← deblocate de US-004; ating aceleasi fisiere → secvential intre ele
```
---
## Raport VERIFY
**Verdict: PASS** — 2026-07-07, E2E in browser (Playwright, uvicorn pe DB temporar, 7 trimiteri
seedate: sent/queued/held/needs_mapping/error, test+prod, multi-cod).
- Desktop 1280px: header Vehicul/Cod RAR/Operatie/Data/Status/Mediu; un rand per trimitere;
bulina colorata cu title; `P` fill rosu / `T` contur; `OE-1 +2` cu tooltip; sub-linie
`ORA-12899...` mono trunchiata doar pe rândul error; `nemapat` italic fara sub-linie.
- Bara bulk: ascunsa implicit; la 2 checkbox-uri → "2 selectate · Aplica acest cod la 2 randuri |
Sterge 2 randuri | Deselecteaza" + hint; aplicat OE-2 → ambele randuri → `queued`, bara dispare.
- Mapari: exemplul "Daca operatia contine «verificare» → Cod RAR = OE-2 Intretinere si revizie"
inlocuieste descrierea.
- Chips "+": adaugare OE-1 → chip evidentiat (chip-nou) + "OE-1 adaugat."; duplicat → "OE-1 este
deja in lista — nu a fost adaugat inca o data."; select gol → "Selecteaza un cod RAR din lista".
- Mobil 390px: fara header, 2 linii, T/P + bulina in dreapta, fara scroll orizontal.
- Suita completa: **1622 passed, 1 skipped**.
**Cauza-radacina REALA a bug-ului "+" (gasita LA VERIFY, nu in analiza statica)**: form-urile
parinte din modalele de editare au `hx-disabled-elt="find button"`; atributul se MOSTENESTE pe
butoanele chips, iar `find button` rezolvat relativ la butonul "+" (fara butoane descendente) →
`null` → TypeError intern htmx 1.9.12 INAINTE de request → nicio cerere, nicio eroare vizibila.
Fix: `hx-disabled-elt="this"` explicit pe toate butoanele hx-post din `_chips_prestatii.html`
(care si dezactiveaza butonul cat cererea e in zbor). No-op-urile silentioase server-side
(cod invalid pe add/add_flat, duplicat) erau defecte reale suplimentare, reparate separat.
Observatii non-blocante: (1) `bulk_message` ("N reusite") dispare imediat — HX-Trigger
`trimiteriChanged` reincarca lista peste raspunsul cu mesaj (comportament pre-existent);
(2) pe mobil, operatia e trunchiata agresiv pe randurile cu buton "Trimite" (spatiul e
consumat de actiune + mediu + status) — acceptat, informatia completa e in modal.

View File

@@ -46,3 +46,39 @@ def test_eticheta_lunga_ramane_pentru_subtext():
assert isinstance(css_class, str) and css_class, (
f"eticheta_stare({status!r}) trebuie sa aiba css_class non-vida la pozitia 3"
)
def test_eroare_bruta_prefera_cauza_tehnica():
import json
from app.web.labels import eroare_bruta
rar_error = json.dumps({
"cod": "RAR_EROARE_SERVER",
"problema": "RAR a esuat la inregistrarea prezentarii",
"cauza": "ORA-12899: value too large for column PRESTATII.COD",
"fix": "",
"field": None,
"message": "ORA-12899: value too large for column PRESTATII.COD",
})
assert eroare_bruta("error", rar_error) == (
"ORA-12899: value too large for column PRESTATII.COD"
)
def test_eroare_bruta_goala_pe_needs_mapping_si_stari_ok():
from app.web.labels import eroare_bruta
assert eroare_bruta("needs_mapping", '{"unmapped": [{"cod_op_service": "X"}]}') == ""
assert eroare_bruta("sent", "orice") == ""
assert eroare_bruta("queued", None) == ""
def test_eroare_bruta_validare_needs_data():
import json
from app.web.labels import eroare_bruta
rar_error = json.dumps([{"field": "vin", "message": "VIN invalid (17 caractere)"}])
assert eroare_bruta("needs_data", rar_error) == "VIN invalid (17 caractere)"
def test_eroare_bruta_nu_arunca_pe_json_corupt():
from app.web.labels import eroare_bruta
assert eroare_bruta("error", "{corupt") == "{corupt"
assert eroare_bruta("error", None) == ""

View File

@@ -212,3 +212,23 @@ def test_fara_operatie_service_cand_lipseste():
d4 = prezentare_din_payload("nu-e-json")
assert d4["op_service_cod"] == ""
assert d4["op_service_denumire"] == ""
def test_coduri_rar_lista_ordonata_dedup():
from app.payload_view import prezentare_din_payload
p = prezentare_din_payload({
"vin": "WVWZZZ1JZXW000111",
"prestatii": [
{"cod_prestatie": "OE-1", "cod_op_service": "A"},
{"cod_prestatie": "oe-2", "cod_op_service": "B"},
{"cod_prestatie": "OE-1", "cod_op_service": "C"}, # duplicat
{"cod_op_service": "D"}, # nemapat, fara cod
],
})
assert p["coduri_rar"] == ["OE-1", "OE-2"]
def test_coduri_rar_gol_fara_prestatii():
from app.payload_view import prezentare_din_payload
assert prezentare_din_payload({"vin": "X"})["coduri_rar"] == []
assert prezentare_din_payload(None)["coduri_rar"] == []

View File

@@ -246,3 +246,34 @@ def test_bulk_scoped_cont(client):
# Randul lui B NEATINS
assert _get_status(sid_b) == "needs_mapping", \
"Randul contului B a fost modificat de contul A — INCALCARE SCOPE!"
def test_bara_bulk_ascunsa_implicit(client):
"""Controalele bulk stau intr-o bara contextuala ascunsa implicit (fara selectie)."""
acct = _create_account_user("bara.hidden@test.com")
_insert_submission(acct)
_login(client, "bara.hidden@test.com")
html = client.get("/_fragments/submissions").text
assert 'id="bulk-bar"' in html, "Bara contextuala bulk lipseste"
m = re.search(r'id="bulk-bar"[^>]*class="([^"]*)"', html) or \
re.search(r'class="([^"]*)"[^>]*id="bulk-bar"', html)
assert m and "bulk-bar" in m.group(1) and "activ" not in m.group(1), (
f"Bara bulk trebuie randata inactiva (ascunsa prin CSS): {m.group(0) if m else 'fara clasa'}"
)
# butonul vechi permanent 'Aplica cod' (fara context) a disparut
assert ">\n Aplica cod\n </button>" not in html
def test_bara_bulk_texte_explicite(client):
"""Bara contine contor, buton explicit 'Aplica acest cod la N randuri',
stergere cu N, 'Deselecteaza' si hint-ul cu ce face actiunea."""
acct = _create_account_user("bara.texte@test.com")
_insert_submission(acct)
_login(client, "bara.texte@test.com")
html = client.get("/_fragments/submissions").text
assert "selectate" in html
assert "Aplica acest cod la" in html
assert "Deselecteaza" in html
assert "operatiilor fara cod" in html, "Hint-ul explicativ lipseste"

View File

@@ -799,3 +799,128 @@ def test_add_extra_semnal_vizibil_cod_invalid(client):
assert "chips-extra-error" in resp.text, (
f"Semnalul 'chips-extra-error' lipsa pentru cod invalid: {resp.text[:300]}"
)
def test_add_per_op_cod_invalid_afiseaza_mesaj(client):
"""Actiunea 'add' (picker per operatie) cu cod invalid da semnal vizibil."""
_create_account_user("add.op.err@test.com")
_login(client, "add.op.err@test.com")
_seed_cod("OE-1", "Schimb ulei")
csrf = _csrf(client)
resp = client.post(
"/form-chips",
data={
"csrf_token": csrf,
"cod_prestatie": [""],
"chip_op_service": ["SchimbUlei"],
"chip_denumire": ["Schimb ulei"],
"chips_action": "add",
"chips_add_op_index": "0",
"chips_add_cod_0": "XX-99", # cod inexistent
},
)
assert resp.status_code == 200
assert "chips-extra-error" in resp.text, (
f"Semnal de eroare lipsa la add cu cod invalid: {resp.text[:300]}"
)
def test_add_flat_cod_invalid_afiseaza_mesaj(client):
"""Actiunea 'add_flat' cu cod invalid da semnal vizibil (nu no-op silentios)."""
_create_account_user("add.flat.err@test.com")
_login(client, "add.flat.err@test.com")
_seed_cod("OE-1", "Schimb ulei")
csrf = _csrf(client)
resp = client.post(
"/form-chips",
data={
"csrf_token": csrf,
"cod_prestatie": ["OE-1"],
"chip_op_service": [""],
"chip_denumire": [""],
"chips_action": "add_flat",
"chips_add_cod_flat": "XX-99",
},
)
assert resp.status_code == 200
assert "chips-extra-error" in resp.text, (
f"Semnal de eroare lipsa la add_flat cu cod invalid: {resp.text[:300]}"
)
def test_add_flat_duplicat_afiseaza_mesaj(client):
"""add_flat cu un cod deja prezent NU dubleaza chip-ul si afiseaza mesaj."""
_create_account_user("add.flat.dup@test.com")
_login(client, "add.flat.dup@test.com")
_seed_cod("OE-1", "Schimb ulei")
csrf = _csrf(client)
resp = client.post(
"/form-chips",
data={
"csrf_token": csrf,
"cod_prestatie": ["OE-1"],
"chip_op_service": [""],
"chip_denumire": [""],
"chips_action": "add_flat",
"chips_add_cod_flat": "OE-1", # deja in lista
},
)
assert resp.status_code == 200
import re as _re
hidden = _re.findall(r'<input[^>]+name="cod_prestatie"[^>]+value="OE-1"', resp.text)
assert len(hidden) == 1, f"Chip OE-1 duplicat: {len(hidden)} aparitii"
assert "deja in lista" in resp.text, (
f"Mesaj de duplicat lipsa la add_flat: {resp.text[:300]}"
)
def test_add_extra_duplicat_afiseaza_mesaj(client):
"""add_extra cu chip extra identic existent afiseaza mesaj (nu no-op)."""
_create_account_user("add.extra.dup@test.com")
_login(client, "add.extra.dup@test.com")
_seed_cod("OE-1", "Schimb ulei")
_seed_cod("FRN1", "Franare")
csrf = _csrf(client)
resp = client.post(
"/form-chips",
data={
"csrf_token": csrf,
"cod_prestatie": ["OE-1", "FRN1"],
"chip_op_service": ["SchimbUlei", ""],
"chip_denumire": ["Schimb ulei", ""],
"chips_action": "add_extra",
"chips_add_cod_flat": "FRN1", # chip extra identic deja existent
},
)
assert resp.status_code == 200
assert "deja in lista" in resp.text, (
f"Mesaj de duplicat lipsa la add_extra: {resp.text[:300]}"
)
def test_add_reusit_marcheaza_chip_nou(client):
"""Adaugare reusita: chip-ul nou e evidentiat (chip-nou) + mesaj de confirmare."""
_create_account_user("add.ok.nou@test.com")
_login(client, "add.ok.nou@test.com")
_seed_cod("OE-1", "Schimb ulei")
_seed_cod("FRN1", "Franare")
csrf = _csrf(client)
resp = client.post(
"/form-chips",
data={
"csrf_token": csrf,
"cod_prestatie": ["OE-1"],
"chip_op_service": ["SchimbUlei"],
"chip_denumire": ["Schimb ulei"],
"chips_action": "add_extra",
"chips_add_cod_flat": "FRN1",
},
)
assert resp.status_code == 200
assert "chip-nou" in resp.text, f"Chip nou neevidentiat: {resp.text[:300]}"
assert "adaugat" in resp.text, f"Mesaj de confirmare lipsa: {resp.text[:300]}"

View File

@@ -198,3 +198,20 @@ def test_cod_invalid_respins(client):
assert resp.status_code == 200
assert "necunoscut" in resp.text.lower()
assert _text_rules(acct) == []
def test_sectiune_reguli_afiseaza_exemplu_nu_descriere(client):
"""Capul sectiunii = un exemplu concret de regula, nu paragraf descriptiv."""
_create_account_user("exemplu.regula@test.com")
_login(client, "exemplu.regula@test.com")
resp = client.get("/?tab=mapari")
assert resp.status_code == 200
html = resp.text
# Exemplul concret e prezent (pattern + cod, formatate)
assert "regula-exemplu" in html, "Blocul de exemplu (regula-exemplu) lipseste"
assert "verificare" in html
assert "OE-2" in html
# Paragraful descriptiv vechi a disparut
assert "nu egal, ci substring" not in html, "Descrierea veche inca prezenta"

View File

@@ -539,7 +539,7 @@ def test_clasa_contor_card(client):
def test_clasa_lista_slim(client):
""".lista-trimiteri-slim + .trimitere-slim: separator --line2, padding, tinta min-height:44px.
Sub-elemente: .slim-vin (mono) si .slim-meta (muted 11px).
Sub-elemente: .slim-vin (mono) si .c-meta (celulele cod/operatie/data).
"""
_create_account_user("ls2@test.com")
_login(client, "ls2@test.com")
@@ -552,8 +552,8 @@ def test_clasa_lista_slim(client):
assert "var(--line2)" in bloc, ".trimitere-slim nu foloseste var(--line2) ca separator"
assert "min-height:44px" in bloc, ".trimitere-slim nu are tinta min-height:44px"
assert ".slim-vin" in bloc, ".slim-vin lipseste din bloc"
assert ".slim-meta" in bloc, ".slim-meta lipseste din bloc"
assert "var(--muted)" in bloc, ".slim-meta nu foloseste var(--muted)"
assert ".c-meta" in bloc, ".c-meta lipseste din bloc"
assert "var(--muted)" in bloc, "componentele slim nu folosesc var(--muted)"
def test_clasa_camp_slim(client):

View File

@@ -186,15 +186,16 @@ def test_operatie_contine_cod_rar(client):
def test_pill_eticheta_scurta(client):
"""Pill-ul de Stare foloseste eticheta scurta; textul lung in title."""
"""Statusul e bulina colorata (dot-stare); textul complet al starii in title."""
acct = _create_account_user("pill@test.com")
_insert_submission(acct, "sent", id_prezentare=70001)
_login(client, "pill@test.com")
resp = client.get("/_fragments/submissions")
assert resp.status_code == 200
html = resp.text
# eticheta scurta in pill
assert ">Finalizat<" in html
# bulina cu clasa de stare, fara text in celula
assert "dot-stare" in html
assert "s-sent" in html
# textul lung pastrat ca tooltip (title)
assert 'title="Declarate la RAR' in html
@@ -229,8 +230,8 @@ def test_eticheta_umana_apare_sub_pill(client):
resp = client.get("/_fragments/submissions")
assert resp.status_code == 200
html = resp.text
assert "eticheta-problema" in html # containerul textului uman sub pill
assert "s-error" in html # colorat ca problema (error/needs_*)
assert "c-err" in html # sub-linia compacta de eroare
assert "lipsa odometru" in html # textul brut al erorii, nu eticheta explicativa
# NU randeaza cod brut de catalog pe rand
assert "COD_" not in html
assert "RAR_EROARE" not in html
@@ -444,16 +445,17 @@ def test_rand_slim_vin_operatie_pill(client):
assert "slim-vin" in html, "slim-vin lipseste — linia 1 placuta"
assert "B777ZZZ" in html, "placuta (nr. inmatriculare) lipseste de pe rand"
# L2: cod RAR · operatie · data (slim-meta / slim-rand2)
assert "slim-meta" in html, "slim-meta lipseste — linia 2"
assert "slim-rand2" in html, "slim-rand2 lipseste — linia 2 (cod RAR · operatie · data)"
# Celulele: cod RAR · operatie · data (c-meta cu c-cod/c-op/c-data)
assert "c-meta" in html, "c-meta lipseste — celulele cod/operatie/data"
assert "c-cod" in html and "c-op" in html, "celulele cod/operatie lipsesc"
# VIN integral nu mai e pe rand — traieste in modalul de detaliu.
assert "000777" not in html, "VIN scurt nu mai trebuie randat pe randul slim (2 linii)"
# Pill de stare: clasa CSS + eticheta scurta
assert "s-sent" in html, "clasa pill s-sent lipseste"
assert "Finalizat" in html, "eticheta scurta stare_scurt lipseste"
# Status: bulina cu clasa de stare; numele coloanei in header
assert "s-sent" in html, "clasa de stare s-sent lipseste"
assert "dot-stare" in html, "bulina de status lipseste"
assert "Status" in html, "headerul coloanei Status lipseste"
def test_filtre_paginare_pastrate(client):

View File

@@ -136,3 +136,128 @@ def test_placuta_lipsa_nu_genereaza_rand_gol(client):
assert m.group(1).strip() != "", "slim-vin afiseaza '' izolat ca identificator"
assert "fara numar" in html, "fallback 'fara numar' lipseste cand placuta+VIN absente"
# --------------------------------------------------------------------------- #
# Tabel compact desktop (PRD 5.22): header coloane, bulina status, T/P, #
# badge +N multi-cod, sub-linie eroare bruta. #
# --------------------------------------------------------------------------- #
def _ins_full(acct: int, *, payload: dict, status: str = "queued",
rar_env: str = "test", rar_error: str | None = None) -> int:
from app.db import get_connection
conn = get_connection()
try:
cur = conn.execute(
"INSERT INTO submissions (idempotency_key, account_id, status, payload_json, rar_env, rar_error) "
"VALUES (?, ?, ?, ?, ?, ?)",
(f"k-{os.urandom(5).hex()}", acct, status,
json.dumps(payload), rar_env, rar_error),
)
conn.commit()
return int(cur.lastrowid)
finally:
conn.close()
def _payload_min(coduri: list[str] | None = None) -> dict:
return {
"vin": "WVWZZZ1JZXW000002",
"nr_inmatriculare": "B22TST",
"data_prestatie": "2026-06-21",
"odometru_final": "200",
"prestatii": [{"cod_prestatie": c} for c in (coduri or ["R-X"])],
}
def test_desktop_header_coloane(client):
"""Lista are un rand de header cu numele coloanelor (Status, Mediu etc.)."""
acct = _create_account_user("head.col@test.com")
_ins(acct, nr="B77HDR")
_login(client, "head.col@test.com")
html = client.get("/_fragments/submissions").text
assert "trimiteri-head" in html, "Randul de header lipseste"
for col in ("Vehicul", "Cod RAR", "Operatie", "Status", "Mediu"):
assert col in html, f"Coloana '{col}' lipseste din header"
def test_status_bulina_cu_tooltip(client):
"""Statusul e o bulina colorata (dot-stare) cu numele starii in title."""
acct = _create_account_user("dot.status@test.com")
_ins_full(acct, payload=_payload_min(), status="sent")
_login(client, "dot.status@test.com")
html = client.get("/_fragments/submissions").text
m = re.search(r'<span[^>]*class="dot-stare[^"]*s-sent[^"]*"[^>]*title="([^"]+)"', html) or \
re.search(r'<span[^>]*class="dot-stare[^"]*"[^>]*class[^>]*>', html)
assert "dot-stare" in html, "Bulina de status (dot-stare) lipseste"
assert m, f"Bulina fara title (tooltip cu starea): {html[html.find('dot-stare')-100:html.find('dot-stare')+200]}"
def test_mediu_litera_t_p(client):
"""Mediul RAR e litera compacta: T (testare, contur) / P (productie, fill)."""
acct = _create_account_user("env.litera@test.com")
_ins_full(acct, payload=_payload_min(), rar_env="test")
_ins_full(acct, payload=_payload_min(), rar_env="prod")
_login(client, "env.litera@test.com")
html = client.get("/_fragments/submissions").text
assert re.search(r'class="env-l env-l-test"[^>]*>\s*T\s*<', html), "Litera T (testare) lipseste"
assert re.search(r'class="env-l env-l-prod"[^>]*>\s*P\s*<', html), "Litera P (productie) lipseste"
def test_multicod_badge_plus_n(client):
"""Mai multe coduri RAR: primul cod + badge +N cu toate codurile in title."""
acct = _create_account_user("multicod@test.com")
_ins_full(acct, payload=_payload_min(["OE-1", "OE-2", "OE-8"]))
_ins_full(acct, payload=_payload_min(["OE-4"]))
_login(client, "multicod@test.com")
html = client.get("/_fragments/submissions").text
m = re.search(r'<span class="cod-plus" title="([^"]+)">\+2</span>', html)
assert m, "Badge-ul +2 lipseste pentru trimiterea cu 3 coduri"
for cod in ("OE-1", "OE-2", "OE-8"):
assert cod in m.group(1), f"Codul {cod} lipseste din tooltip-ul badge-ului"
# un singur cod -> fara badge
assert not re.search(r'OE-4[^<]*</span><span class="cod-plus"', html)
def test_sublinie_eroare_bruta_doar_pe_error(client):
"""Sub-linia rosie contine textul brut al erorii RAR, doar pe error/needs_data."""
acct = _create_account_user("err.brut@test.com")
_ins_full(
acct, payload=_payload_min(), status="error",
rar_error=json.dumps({
"cod": "RAR_EROARE_SERVER",
"problema": "RAR a esuat la inregistrarea prezentarii",
"cauza": "ORA-12899: value too large for column PRESTATII.COD",
"fix": "x", "field": None,
"message": "ORA-12899: value too large for column PRESTATII.COD",
}),
)
_ins_full(
acct,
payload={**_payload_min(), "prestatii": [{"cod_op_service": "IGIENIZARE", "denumire": "Igienizare AC"}]},
status="needs_mapping",
rar_error=json.dumps({"unmapped": [{"cod_op_service": "IGIENIZARE"}]}),
)
_login(client, "err.brut@test.com")
html = client.get("/_fragments/submissions").text
err_cells = re.findall(r'class="c-err"[^>]*>([^<]*)<', html)
assert len(err_cells) == 1, f"Exact o sub-linie de eroare asteptata, gasite {len(err_cells)}"
assert "ORA-12899" in err_cells[0], f"Textul brut RAR lipseste: {err_cells[0]!r}"
def test_mobil_pastreaza_doua_linii_cu_bulina_si_litera(client):
"""CSS-ul mobil: header ascuns, rand pe 2 linii prin grid-areas (veh / meta),
cu actiune + mediu + status in dreapta."""
_create_account_user("mobil.css@test.com")
_login(client, "mobil.css@test.com")
html = client.get("/?tab=coada").text
assert re.search(r'\.trimiteri-head\s*\{\s*display\s*:\s*none', html), \
"Headerul de coloane nu e ascuns pe mobil"
m = re.search(r'grid-template-areas\s*:\s*"check veh\s+act env status"\s*"check meta act env status"', html)
assert m, "Grid-areas pe 2 linii (veh/meta) lipseste din CSS-ul mobil"