feat(5.16): aliniere lista/preview la mockup + fix lock seed la boot

Implementeaza planul aprobat din docs/raport-comparatie-mockup-5.16.md (T-1..T-9):

- T-1/T-8: rand lista 4->2 linii (placuta primar + cod RAR · operatie · data + pill),
  fallback placuta, eticheta-problema 10px->--fs-xs (_submissions.html, base.html)
- T-2: pill slim restilat fill-tint + dot 7px + text colorat per stare (base.html)
- T-3: bug 4a coliziune pill/vehicul in preview — col-stare 104->140px (base.html)
- T-4: preview 8->5 coloane (scos #, KM, Note; motivul -> title pe pill)
- T-5: titlu sectiune "Trimiterile tale" -> sr-only (a11y) + badge/export discret
- T-6: linia plan N/60 in corp doar pe avertizare; consum normal in badge+burger
- T-7: guard chenar gol chips extra (_chips_prestatii.html)
- T-9: "Anuleaza"->"Renunta"; nume operatie emfatic bold

Fix boot: init_db reincarca seedul de ~17k operatii (5.18) pe FIECARE pornire, pe
API + worker concurent -> "database is locked" la al doilea proces. Guard "_if_empty"
pe mapping_suggestions (ca seed_nomenclator_if_empty) -> boot rapid, fara cursa.

Teste actualizate (slim 2-linii, fallback placuta, plan in burger). TODOS.md:
defer trackuit (eroare HTMX lista, retokenizare px, diacritice).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-06-29 14:44:10 +00:00
parent e1243f603e
commit 8f39dfbc1e
14 changed files with 212 additions and 130 deletions

View File

@@ -53,3 +53,26 @@ Elemente deferate din review-uri. Negrupte de un PRD curent; de promovat cand de
- [ ] **US-009/US-010 ca PRD separat daca propagarea design e urgenta** — salvarea mapare-din-chip si - [ ] **US-009/US-010 ca PRD separat daca propagarea design e urgenta** — salvarea mapare-din-chip si
bulk-fix sunt adiacente FUNCTIONALE (acceptate via SELECTIVE EXPANSION), dincolo de obiectivul pur de bulk-fix sunt adiacente FUNCTIONALE (acceptate via SELECTIVE EXPANSION), dincolo de obiectivul pur de
propagare design. Daca vrei sa livrezi designul rapid, pot fi scoase intr-un PRD propriu. (CEO, low.) propagare design. Daca vrei sa livrezi designul rapid, pot fi scoase intr-un PRD propriu. (CEO, low.)
## Din raport comparatie mockup 5.16 (2026-06-29)
> Restul task-urilor din `docs/raport-comparatie-mockup-5.16.md` au fost livrate (T-1..T-9).
> Cele de mai jos raman explicit in coada la cererea userului.
- [ ] **Stare de eroare HTMX la incarcarea listei (D-4)** — cand `/_fragments/submissions`
da 500 sau pica reteaua, `#submissions-wrap` ramane blocat pe spinner ("se incarca…") fara
mesaj. De adaugat un partial de eroare / `hx-on::response-error` cu "nu s-a putut incarca,
reincearca". Robustete pre-existenta (nu introdusa de 5.16), impact functional real —
**candidatul cu cea mai mare valoare** din lista. (Design D-4, medium.)
- [ ] **Retokenizare px completa in template-uri**`_submissions.html` / `_preview_*` folosesc
literali `font-size:13px/12px/11px` in loc de token-urile `--fs-*`. 5.16 a corectat doar
instanta sub-12px (incalca pragul PRD). Restul ramane debt: schimbarea in masa (13px→`--fs-sm`
=13.5px) misca layout-ul, deci necesita o baza de regresie vizuala inainte. (Eng, bounded —
amanat ca scope creep fara baza AC.)
- [ ] **Diacritice in textul vizibil pentru user** — mockup-urile folosesc diacritice complete
("Observații", "Salvează", "Adaugă"); aplicatia le omite in majoritatea label-urilor. Fontul
le randeaza corect (US-001 confirmat). De aplicat pe label-uri/butoane/titluri, pastrand
cod/comentariile fara diacritice. Decizie initiala (poarta de gust T3): nu se aplica acum —
reintrodus in coada la cererea userului (2026-06-29) ca finisaj viitor. (Transversal, low.)

View File

@@ -39,10 +39,19 @@ def init_db() -> None:
seed_nomenclator_if_empty(conn) seed_nomenclator_if_empty(conn)
# Seed corpus operatii etichetate -> mapping_suggestions (SILVER, PRD 5.18 US-004). # Seed corpus operatii etichetate -> mapping_suggestions (SILVER, PRD 5.18 US-004).
# Gated: OFF in teste (conftest), ON in productie. INSERT OR IGNORE -> idempotent. # Gated: OFF in teste (conftest), ON in productie. INSERT OR IGNORE -> idempotent.
# DOAR daca mapping_suggestions e gol: seedul are ~17k randuri; re-rularea lui pe
# FIECARE boot (API + worker concurent) tinea write-lock-ul indelung -> al doilea
# proces primea "database is locked" la pornire. Guard "_if_empty" (ca nomenclatorul)
# -> boot rapid cand e deja seeded. Re-seed dupa actualizarea fisierului = manual
# (goleste tabela), consistent cu semantica v1 ignore-not-upsert a seederului.
if get_settings().seed_operatii_enabled: if get_settings().seed_operatii_enabled:
from .operatii_seed import seed_operatii_etichetate already = conn.execute(
"SELECT 1 FROM mapping_suggestions LIMIT 1"
).fetchone()
if not already:
from .operatii_seed import seed_operatii_etichetate
seed_operatii_etichetate(conn) seed_operatii_etichetate(conn)
conn.commit() conn.commit()
finally: finally:
conn.close() conn.close()

View File

@@ -118,10 +118,13 @@
{% endfor %} {% endfor %}
{# ===== US-005 (5.16): Chips extra + picker '+ Adauga alta operatie / cod RAR' in mod operatii ===== #} {# ===== US-005 (5.16): Chips extra + picker '+ Adauga alta operatie / cod RAR' in mod operatii ===== #}
{# Chips extra: cod_op_service gol, cod_prestatie setat — afisate flat cu × (reuse remove_flat) #} {# Chips extra: cod_op_service gol, cod_prestatie setat — afisate flat cu × (reuse remove_flat).
T-7 (5.16): containerul .chips se randeaza DOAR cand exista chips extra — altfel ramanea
un chenar gol nefinisat sub randurile de operatie. #}
{% set _extra_chips = _chips | rejectattr('cod_op_service') | selectattr('cod_prestatie') | list %}
{% if _extra_chips %}
<div class="chips" role="group" aria-label="Coduri RAR suplimentare" style="margin-top:4px;"> <div class="chips" role="group" aria-label="Coduri RAR suplimentare" style="margin-top:4px;">
{% for chip in _chips %} {% for chip in _extra_chips %}
{% if not chip.cod_op_service and chip.cod_prestatie %}
{% set _is_warn_extra = chip.cod_prestatie in ('R-ODO', 'I-ODO') %} {% set _is_warn_extra = chip.cod_prestatie in ('R-ODO', 'I-ODO') %}
<span class="chip {% if _is_warn_extra %}chip-warn{% endif %}" <span class="chip {% if _is_warn_extra %}chip-warn{% endif %}"
aria-label="Cod RAR suplimentar {{ chip.cod_prestatie }}"> aria-label="Cod RAR suplimentar {{ chip.cod_prestatie }}">
@@ -134,9 +137,9 @@
hx-vals='{"chips_action":"remove_flat","chips_remove_cod":"{{ chip.cod_prestatie }}"}' hx-vals='{"chips_action":"remove_flat","chips_remove_cod":"{{ chip.cod_prestatie }}"}'
aria-label="Sterge codul suplimentar {{ chip.cod_prestatie }}">&times;</button> aria-label="Sterge codul suplimentar {{ chip.cod_prestatie }}">&times;</button>
</span> </span>
{% endif %}
{% endfor %} {% endfor %}
</div> </div>
{% endif %}
{% if nomenclator_rar %} {% if nomenclator_rar %}
<span style="display:inline-flex;align-items:center;gap:4px;margin-top:4px;"> <span style="display:inline-flex;align-items:center;gap:4px;margin-top:4px;">
<select name="chips_add_cod_flat" <select name="chips_add_cod_flat"

View File

@@ -6,19 +6,26 @@
style="margin-top:22px; padding-top:18px; border-top:2px solid var(--line);" style="margin-top:22px; padding-top:18px; border-top:2px solid var(--line);"
{% if oob %}hx-swap-oob="outerHTML"{% endif %}> {% if oob %}hx-swap-oob="outerHTML"{% endif %}>
<div class="card"> <div class="card">
<div style="display:flex; align-items:center; gap:8px; flex-wrap:wrap; margin:0 0 12px;"> {# US-002 (5.16): titlul de sectiune vizibil ("Trimiterile tale") a fost eliminat —
<h2 id="trimiteri-heading" style="font-size:15px; margin:0;"> lista incepe direct sub filtre. Heading pastrat sr-only pentru a11y (section
Trimiterile tale aria-labelledby). Badge-ul de atentie + export CSV stau intr-un rand discret. #}
{% if blocate_total %} <h2 id="trimiteri-heading" class="sr-only">Trimiterile tale</h2>
<span class="tab-badge" title="{{ blocate_total }} necesita atentie" {% if blocate_total %}
style="display:inline-flex; align-items:center; justify-content:center; min-width:18px; height:18px; margin-left:6px; padding:0 5px; border-radius:99px; background:var(--err); color:#fff; font-size:11px; font-weight:700;">{{ blocate_total }}</span> <div style="display:flex; align-items:center; gap:6px; flex-wrap:wrap; margin:0 0 10px;">
{% endif %} <span class="tab-badge" title="{{ blocate_total }} necesita atentie"
</h2> style="display:inline-flex; align-items:center; justify-content:center; min-width:18px; height:18px; padding:0 5px; border-radius:99px; background:var(--err); color:#fff; font-size:11px; font-weight:700;">{{ blocate_total }}</span>
<span class="muted" style="font-size:var(--fs-sm);">de rezolvat</span>
<span style="margin-left:auto; display:flex; gap:8px; flex-wrap:wrap;"> <span style="margin-left:auto; display:flex; gap:8px; flex-wrap:wrap;">
<a class="cardlink" href="/v1/audit/export?status=sent" download>export CSV: trimise</a> <a class="cardlink" href="/v1/audit/export?status=sent" download>export CSV: trimise</a>
<a class="cardlink" href="/v1/audit/export?status=all" download>toate</a> <a class="cardlink" href="/v1/audit/export?status=all" download>toate</a>
</span> </span>
</div> </div>
{% else %}
<div style="display:flex; justify-content:flex-end; gap:8px; flex-wrap:wrap; margin:0 0 10px;">
<a class="cardlink" href="/v1/audit/export?status=sent" download>export CSV: trimise</a>
<a class="cardlink" href="/v1/audit/export?status=all" download>toate</a>
</div>
{% endif %}
<!-- Bara de filtre: [quick-pills data STANGA] [cautare vehicul MIJLOC] [pills stare DREAPTA]. <!-- Bara de filtre: [quick-pills data STANGA] [cautare vehicul MIJLOC] [pills stare DREAPTA].
Pill-urile de stare scriu campul hidden status si re-trimit form-ul (filtreazaStare). Pill-urile de stare scriu campul hidden status si re-trimit form-ul (filtreazaStare).

View File

@@ -103,8 +103,8 @@
<div class="act-group" style="margin-top:14px;"> <div class="act-group" style="margin-top:14px;">
<button type="submit" class="act act-primary" aria-label="{{ btn_label or 'Salveaza' }}"> <button type="submit" class="act act-primary" aria-label="{{ btn_label or 'Salveaza' }}">
<span class="act-tx">{{ btn_label or 'Salveaza' }}</span>{{ icon('save') }}</button> <span class="act-tx">{{ btn_label or 'Salveaza' }}</span>{{ icon('save') }}</button>
<button type="button" class="act" aria-label="{{ cancel_label or 'Anuleaza' }}" data-modal-close> <button type="button" class="act" aria-label="{{ cancel_label or 'Renunta' }}" data-modal-close>
<span class="act-tx">{{ cancel_label or 'Anuleaza' }}</span>{{ icon('x') }}</button> <span class="act-tx">{{ cancel_label or 'Renunta' }}</span>{{ icon('x') }}</button>
</div> </div>
{% else %} {% else %}
<div style="margin-top:14px;"> <div style="margin-top:14px;">

View File

@@ -144,19 +144,18 @@
{% endif %} {% endif %}
<!-- Tabel preview in format identic cu tabelul Trimiteri (.tabel-trimiteri). <!-- Tabel preview in format identic cu tabelul Trimiteri (.tabel-trimiteri).
US-007: 8 coloane (coloana de verificare eliminata). 5.16 (T-4): densitate redusa la coloanele esentiale — Stare / Vehicul /
Randurile au FORM PROPRIU pentru editare (NU sunt in #confirm-form). --> Operatie / Data + Actiuni. KM final + mesajul de validare (Note) au iesit
din tabel: KM se editeaza in modal, motivul apare ca tooltip pe pill-ul de
Stare. Randurile au FORM PROPRIU pentru editare (NU sunt in #confirm-form). -->
<div id="preview-tabel" class="tablewrap tabel-trimiteri"> <div id="preview-tabel" class="tablewrap tabel-trimiteri">
<table> <table>
<thead> <thead>
<tr> <tr>
<th class="col-id">#</th>
<th class="col-stare">Stare</th> <th class="col-stare">Stare</th>
<th class="col-vehicul">Vehicul</th> <th class="col-vehicul">Vehicul</th>
<th class="col-operatie">Operatie</th> <th class="col-operatie">Operatie</th>
<th class="col-data">Data</th> <th class="col-data">Data</th>
<th class="col-km">KM final</th>
<th class="col-note">Note</th>
<th class="col-actiuni">Actiuni</th> <th class="col-actiuni">Actiuni</th>
</tr> </tr>
</thead> </thead>

View File

@@ -23,9 +23,21 @@
{% if _sent_dup %}class="preview-sent-row"{% endif %} {% if _sent_dup %}class="preview-sent-row"{% endif %}
{% if oob_tr %}hx-swap-oob="outerHTML:#preview-row-{{ row.row_index }}"{% endif %} {% if oob_tr %}hx-swap-oob="outerHTML:#preview-row-{{ row.row_index }}"{% endif %}
style="{% if status == 'needs_review' %}background:rgba(230,179,74,.04);{% elif _sent_dup %}opacity:.6;{% endif %}"> style="{% if status == 'needs_review' %}background:rgba(230,179,74,.04);{% elif _sent_dup %}opacity:.6;{% endif %}">
<td class="col-id muted" data-eticheta="#">{{ row.row_index + 1 }}</td> {#- Motivul (validare / deja-trimis / duplicat) — fost coloana Note, acum tooltip pe pill.
KM final iese din tabel (se editeaza in modal). -#}
{%- if status == 'already_sent' and row.get('already_sent_info') -%}
{%- set ai = row.already_sent_info -%}
{%- set _nota = 'deja trimis ' ~ ((ai.get('created_at') or '')[:10]) ~ ((' (#' ~ ai.id_prezentare ~ ')') if ai.get('id_prezentare') else '') -%}
{%- elif status == 'duplicate_in_file' and row.get('duplicate_with') -%}
{%- set _dwith = [] -%}
{%- for idx in row.duplicate_with -%}{{ _dwith.append(idx + 1) or '' }}{%- endfor -%}
{%- set _nota = 'dubla cu randul ' ~ (_dwith | join(', ')) -%}
{%- else -%}
{%- set _nota = row.nota_umana or '' -%}
{%- endif -%}
<td class="col-stare" data-eticheta="Stare"> <td class="col-stare" data-eticheta="Stare">
<span class="pill {{ row.stare_css }}" style="display:inline-flex; align-items:center; gap:5px;"> <span class="pill {{ row.stare_css }}" style="display:inline-flex; align-items:center; gap:5px;"
{% if _nota %}title="{{ _nota }}"{% endif %}>
<span aria-hidden="true" style="display:inline-block; width:7px; height:7px; border-radius:99px; background:currentColor; flex-shrink:0;"></span>{{ row.stare_eticheta }}</span> <span aria-hidden="true" style="display:inline-block; width:7px; height:7px; border-radius:99px; background:currentColor; flex-shrink:0;"></span>{{ row.stare_eticheta }}</span>
</td> </td>
<td class="col-vehicul" data-eticheta="Vehicul"> <td class="col-vehicul" data-eticheta="Vehicul">
@@ -43,20 +55,6 @@
{% endif %} {% endif %}
</td> </td>
<td class="col-data" data-eticheta="Data prestatie">{{ row.prez.data_prestatie }}</td> <td class="col-data" data-eticheta="Data prestatie">{{ row.prez.data_prestatie }}</td>
<td class="col-km" data-eticheta="KM final">{{ row.prez.odometru }}</td>
<td class="col-note" data-eticheta="Note"
style="font-size:var(--fs-xs); white-space:normal;">
{% if status == 'already_sent' and row.get('already_sent_info') %}
{% set ai = row.already_sent_info %}
deja trimis {{ (ai.get('created_at') or '')[:10] }}
{% if ai.get('id_prezentare') %}(#{{ ai.id_prezentare }}){% endif %}
{% elif status == 'duplicate_in_file' and row.get('duplicate_with') %}
dubla cu randul
{% for idx in row.duplicate_with %}{{ idx + 1 }}{% if not loop.last %}, {% endif %}{% endfor %}
{% else %}
{{ row.nota_umana or '' }}
{% endif %}
</td>
<td class="col-actiuni" data-eticheta="Actiuni" style="text-align:center;"> <td class="col-actiuni" data-eticheta="Actiuni" style="text-align:center;">
{% if status not in ('already_sent', 'duplicate_in_file') %} {% if status not in ('already_sent', 'duplicate_in_file') %}
<button type="button" class="btn-editeaza" <button type="button" class="btn-editeaza"

View File

@@ -137,10 +137,11 @@
class="status-nav-link{% if _tab == 'mapari' %} status-nav-activ{% endif %}">Mapari{% if mapari_badge %}<span class="tab-badge" aria-hidden="true" style="display:inline-flex; align-items:center; justify-content:center; min-width:16px; height:16px; margin-left:4px; padding:0 4px; border-radius:99px; background:var(--err); color:#fff; font-size:11px; font-weight:700;">{{ mapari_badge }}</span>{% endif %}</a> class="status-nav-link{% if _tab == 'mapari' %} status-nav-activ{% endif %}">Mapari{% if mapari_badge %}<span class="tab-badge" aria-hidden="true" style="display:inline-flex; align-items:center; justify-content:center; min-width:16px; height:16px; margin-left:4px; padding:0 4px; border-radius:99px; background:var(--err); color:#fff; font-size:11px; font-weight:700;">{{ mapari_badge }}</span>{% endif %}</a>
</nav> </nav>
{# US-006 (5.17): linia de plan — consum/trial (secundar, sub navigatie, non-blocant). {# US-006 (5.17) + T-6 (5.16): linia de plan in CORP apare DOAR in starea de avertizare
Warn=culoare+text (accesibilitate): >=80% -> --warn; limita atinsa -> --err. (>=80% -> --warn; limita atinsa -> --err). Consumul normal (N/60) traieste in badge-ul
Ierarhie: nu concureaza cu stripul de sanatate (E zero-silent-failures pastrat). #} din antet + linia din meniul burger, nu ca rand permanent in corp (densitate redusa).
{% if plan_linie is defined and plan_linie %} Ierarhie: nu concureaza cu stripul de sanatate (zero-silent-failures pastrat). #}
{% if plan_linie and (plan_warn|default(false) or plan_limita_atinsa|default(false)) %}
<div class="plan-status-line" <div class="plan-status-line"
style="font-size:var(--fs-sm); margin-top:6px; padding-top:6px; style="font-size:var(--fs-sm); margin-top:6px; padding-top:6px;
border-top:1px solid var(--line2); border-top:1px solid var(--line2);

View File

@@ -96,41 +96,43 @@
{% endif %} {% endif %}
</div> </div>
{# Bloc text principal — stanga, ocupa spatiul ramas #} {# 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;"> <div style="flex:1 1 auto; min-width:0;">
{# Linia 1: VIN mono scurt (slim-vin). {# Linia 1: nr. inmatriculare (placuta) — identificatorul primar pe care il
Guard: vin_scurt='—' inseamna VIN lipsa; fallback la vehicul_nr. #} scaneaza operatorul. .slim-vin reumplut (acelasi nume de clasa, churn minim).
{% if r.prez.vin_scurt and r.prez.vin_scurt != '—' %} Fallback cand placuta lipseste ('—'): VIN scurt, apoi mesaj neutru
<div class="slim-vin">{{ r.prez.vin_scurt }}</div> (nu randa em-dash izolat ca identificator). #}
{% 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 != '—' %}
<div class="slim-vin muted">{{ r.prez.vin_scurt }}</div>
{% else %} {% else %}
<div class="slim-vin muted">{{ r.prez.vehicul_nr }}</div> <div class="slim-vin muted">fara numar</div>
{% endif %} {% endif %}
{# Linia 2: Operatie · ora/data (slim-meta muted) #} {# Linia 2: cod RAR (sau 'nemapat') · operatie (ink, ellipsis) · data prestatie.
<div class="slim-meta">{{ r.prez.operatie }} · {{ r.updated_at }}</div> Separatorul "·" e injectat prin CSS intre celule. Operatia primeste ellipsis
ca randul sa NU treaca pe a 3-a linie nici la 390px.
{# Cod RAR sau indicatorul 'nemapat': discret sub operatie. VIN integral, #id_prezentare si secundele traiesc in modalul de detaliu. #}
Mentine compatibilitatea cu testele cod_rar: OE-2 vizibil, fara prefix 'cod RAR:'. #} <div class="slim-meta slim-rand2">
{% if r.prez.cod_rar and r.prez.cod_rar != '—' %} {% if r.prez.cod_rar and r.prez.cod_rar != '—' %}
<div class="slim-meta"><span class="cod-rar-cod">{{ r.prez.cod_rar }}</span></div> <span class="cod-rar-cod">{{ r.prez.cod_rar }}</span>
{% else %} {% else %}
<div class="slim-meta muted cod-rar-sub">nemapat</div> <span class="cod-rar-cod cod-rar-sub muted">nemapat</span>
{% endif %} {% endif %}
<span class="slim-op">{{ r.prez.operatie }}</span>
{# Linia meta discreta: nr inmatriculare · data prestatie · nr prezentare RAR. {% if r.prez.data_prestatie and r.prez.data_prestatie != '—' %}
Accesibila pe rand; informatia completa e in modalul de detaliu. #} <span class="slim-data muted">{{ r.prez.data_prestatie }}</span>
<div class="slim-meta" style="opacity:0.7;"> {% endif %}
{{ r.prez.vehicul_nr -}}
{%- if r.prez.data_prestatie and r.prez.data_prestatie != '—' %} · {{ r.prez.data_prestatie }}{% endif -%}
{%- if r.id_prezentare %} · #{{ r.id_prezentare }}{% endif %}
</div> </div>
{# Eticheta umana scurta sub pill — text mic, s-error pe error/needs_*. {# Micro-linie umana a problemei — text mic s-error, DOAR pe stari de problema
Afisata DOAR pe randuri cu problema (eticheta_problema ne-goala). (loud-on-exception D6). Randul normal/finalizat ramane strict 2 linii.
Starea transmisa prin TEXT, nu doar culoare. #} 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 %} {% if r.eticheta_problema and r.eticheta_problema != r.stare_scurt and r.eticheta_problema != r.stare_text %}
<div class="eticheta-problema s-error" style="font-size:10px; margin-top:2px;">{{ r.eticheta_problema }}</div> <div class="eticheta-problema s-error">{{ r.eticheta_problema }}</div>
{% endif %} {% endif %}
</div> </div>

View File

@@ -143,6 +143,9 @@
.s-needs_review{color:var(--warn);} .s-needs_review{color:var(--warn);}
.s-already_sent,.s-duplicate_in_file{color:var(--muted);} .s-already_sent,.s-duplicate_in_file{color:var(--muted);}
.muted { color:var(--muted); } .muted { color:var(--muted); }
/* Heading/eticheta accesibila doar pentru cititoare de ecran (vizual ascunsa). */
.sr-only { position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden;
clip:rect(0 0 0 0); white-space:nowrap; border:0; }
a { color:var(--accent); } a { color:var(--accent); }
/* Drop zone upload fisier */ /* Drop zone upload fisier */
.drop-zone { border:2px dashed var(--line); border-radius:8px; padding:32px 20px; .drop-zone { border:2px dashed var(--line); border-radius:8px; padding:32px 20px;
@@ -410,7 +413,9 @@
.tabel-trimiteri th, .tabel-trimiteri td { white-space:normal; word-break:break-word; vertical-align:top; } .tabel-trimiteri th, .tabel-trimiteri td { white-space:normal; word-break:break-word; vertical-align:top; }
.tabel-trimiteri .col-chk { width:30px; } .tabel-trimiteri .col-chk { width:30px; }
.tabel-trimiteri .col-id { width:48px; } .tabel-trimiteri .col-id { width:48px; }
.tabel-trimiteri .col-stare { width:104px; } /* col-stare largita (bug 4a 5.16): cu table-layout:fixed + pill nowrap, 104px era
prea ingusta -> pill-ul de stare se revarsa peste col-vehicul. 140px o contine. */
.tabel-trimiteri .col-stare { width:140px; }
.tabel-trimiteri .col-data { width:104px; } .tabel-trimiteri .col-data { width:104px; }
.tabel-trimiteri .col-rar { width:96px; } .tabel-trimiteri .col-rar { width:96px; }
.tabel-trimiteri .col-actualizat { width:128px; } .tabel-trimiteri .col-actualizat { width:128px; }
@@ -722,8 +727,27 @@
.trimitere-slim:last-child { border-bottom:none; } .trimitere-slim:last-child { border-bottom:none; }
.trimitere-slim:hover { background:color-mix(in srgb, var(--accent) 6%, transparent); } .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; } .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:500; color:var(--ink); } .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; } .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; }
/* .camp-slim — varianta compacta camp formular: label --fs-sm muted deasupra, input --fs-md, fundal --card2. /* .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. */ Mono pentru campuri VIN/odometru/nr: adauga clasa .camp-mono pe input. */
.camp-slim { margin-bottom:8px; } .camp-slim { margin-bottom:8px; }
@@ -756,7 +780,9 @@
.op-row { display:flex; align-items:center; justify-content:space-between; gap:10px; .op-row { display:flex; align-items:center; justify-content:space-between; gap:10px;
padding:8px 10px; border:1px solid var(--line); border-radius:6px; padding:8px 10px; border:1px solid var(--line); border-radius:6px;
background:var(--card2); margin-bottom:8px; } background:var(--card2); margin-bottom:8px; }
.op-row-name { font-size:var(--fs-xs); font-weight:500; color:var(--ink); } /* Nume operatie emfatic (T-9 5.16): proeminent (bold) ca in mockup — e ancora
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)); } .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: tinta touch pentru trimitere-slim (deja garantata prin min-height:44px in regula de baza) */
@media (max-width:767px) { @media (max-width:767px) {

View File

@@ -471,17 +471,18 @@ risc regresie vizuala fara baza AC). **Auto-decis: doar instanta sub-12px** (`et
- **Fallback**: `vehicul_nr == '—'` → nu randa em-dash singur (mesaj fallback). - **Fallback**: `vehicul_nr == '—'` → nu randa em-dash singur (mesaj fallback).
- Pastreaza numele claselor `slim-vin`/`slim-meta` (reumple, nu redenumi) — minimizeaza churn teste. - Pastreaza numele claselor `slim-vin`/`slim-meta` (reumple, nu redenumi) — minimizeaza churn teste.
### Implementation Tasks (agregat) ### Implementation Tasks (agregat) — LIVRAT 2026-06-29 (toate verzi, 1392 teste)
- [ ] **T-1 (INALTA) — `_submissions.html`** — refactor rand 4→2 linii cu placuta+codRAR+operatie+data_prestatie+pill; fallback placuta; pastreaza clase. Update teste: rescrie test_vin_pe_rand_separat_sub_nr, test_rand_slim_vin_operatie_pill, test_submissions_coloane_umane; adauga test 2-linii + test fallback placuta. - [x] **T-1 (INALTA) — `_submissions.html`** — refactor rand 4→2 linii cu placuta+codRAR+operatie+data_prestatie+pill; fallback placuta; clase pastrate. Teste rescrise: test_rand_slim_vin_operatie_pill, test_submissions_coloane_umane, test_placuta_pe_rand_identificator_primar (fost test_vin_pe_rand_separat_sub_nr), test_placuta_lipsa_nu_genereaza_rand_gol (fallback "fara numar").
- [ ] **T-2 (INALTA) — `base.html` (CSS pill) + `_submissions.html`** — restilare pill slim ca mockup (fill tint + dot + text colorat per `stare_css`); pill ramane pe finalizat. - [x] **T-2 (INALTA) — `base.html` (CSS pill) + `_submissions.html`** — pill slim restilat (fill tint + dot 7px + text colorat per `stare_css` via currentColor), scopat `.lista-trimiteri-slim .pill`; ramane pe finalizat.
- [ ] **T-3 (INALTA) — `_preview_import.html` / `base.html:401`** — bug 4a: `.col-stare` width 104px→~140px (+ `overflow:hidden` sau pill wrap). NU atinge nowrap pe col-vehicul (test_web_preview_compact). Reducere 8→4 coloane (densitate) ca task separat. - [x] **T-3 (INALTA) — `base.html`** — bug 4a: `.tabel-trimiteri .col-stare` 104px→140px. nowrap pe col-vehicul neatins.
- [ ] **T-4 (MEDIE) — `_preview_import.html`** — reducere la 4 coloane esentiale (Stare/Vehicul/Operatie/Data + Editeaza); muta KM + mesaj validare in randul de editare/tooltip. - [x] **T-4 (MEDIE) — `_preview_import.html` + `_preview_rand.html`** — reducere la 5 coloane (Stare/Vehicul/Operatie/Data/Actiuni); scoase col-id, col-km, col-note; motivul mutat in `title` pe pill, KM in modal.
- [ ] **T-5 (MEDIE) — `_coada.html:10-19`** — scoate titlul "Trimiterile tale" (h2); relocare export CSV langa tab-uri / meniu cont (PRD 5.16/US-002). - [x] **T-5 (MEDIE) — `_coada.html`** — titlu vizibil "Trimiterile tale" → `<h2 class="sr-only">` (a11y pastrat); badge "de rezolvat" + export CSV intr-un rand discret. `.sr-only` adaugat in base.html.
- [ ] **T-6 (MEDIE) — `_status.html:140`** — scoate randul plan "N/60 luna asta" din corp; pastreaza badge antet + linie burger (PRD 5.17/US-006). Daca >=80% consum, afiseaza doar in starea de avertizare. - [x] **T-6 (MEDIE) — `_status.html`** — linia plan in corp DOAR pe avertizare (`plan_warn`/`plan_limita_atinsa`); consum normal in badge antet + burger. Teste status mutate pe pagina completa.
- [ ] **T-7 (MEDIE) — `_chips_prestatii.html:122`** — guard `{% if _extra %}` pe containerul `.chips` (operatii-mode), elimina chenarul gol. - [x] **T-7 (MEDIE) — `_chips_prestatii.html`** — guard `{% if _extra_chips %}` pe containerul `.chips`, chenarul gol eliminat.
- [ ] **T-8 (MICA) — `_submissions.html:133`** — `font-size:10px`→`var(--fs-xs)` (doar instanta sub-12px). - [x] **T-8 (MICA) — `_submissions.html` / base.html** — `font-size:10px`→`var(--fs-xs)` (eticheta-problema, prin clasa scopata `.lista-trimiteri-slim .eticheta-problema`).
- [ ] **T-9 (MICA) — copy/stil** — "Anuleaza"→"Renunta" (form editare); nume operatie emfatic (bold) in editorul de chips per mockup. - [x] **T-9 (MICA) — `_form_editare.html` + base.html** — "Anuleaza"→"Renunta" (default); `.op-row-name` emfatic (bold, `--fs-sm`).
- [ ] **Defer TODOS** — stare eroare HTMX lista (D-4); teste regresie vizuala; dropzone zona-mare (sec.5); retokenizare px completa; diacritice (decis: nu). - [ ] **Defer — tracked in `TODOS.md`** (la cererea userului 2026-06-29): stare eroare HTMX lista (D-4); retokenizare px completa; diacritice in textul vizibil.
- [x] **Defer — inchis ca acceptabil** (netrackuit): teste regresie vizuala (tooling viitor); dropzone zona-mare (sec.5, raportul il marcheaza acceptabil).
### Verificare la implementare ### Verificare la implementare
`python3 -m pytest tests/test_web_submissions.py tests/test_web_submissions_layout.py tests/test_web_responsive.py tests/test_web_preview_compact.py -q` `python3 -m pytest tests/test_web_submissions.py tests/test_web_submissions_layout.py tests/test_web_responsive.py tests/test_web_preview_compact.py -q`

View File

@@ -495,7 +495,9 @@ def _insert_submissions_sent(account_id: int, n: int) -> None:
def test_afisaj_plan_si_zile_trial(client): def test_afisaj_plan_si_zile_trial(client):
"""US-006: cont in trial Pro -> fragment status arata 'trial N zile ramase'. """US-006 + T-6 (5.16): cont in trial Pro -> linia de plan din meniul burger (pagina
completa) arata 'Plan: Pro · trial N zile ramase'. In starea normala (non-warn) plan_linie
NU mai e rand in corpul fragmentului status — traieste in badge antet + burger.
Contul nou primeste trial_until=now+30z automat la creare. Contul nou primeste trial_until=now+30z automat la creare.
""" """
acct_id, _ = _create_account_user("trialzile@test.com") acct_id, _ = _create_account_user("trialzile@test.com")
@@ -505,7 +507,7 @@ def test_afisaj_plan_si_zile_trial(client):
future = (datetime.now(timezone.utc) + timedelta(days=18, hours=12)).strftime("%Y-%m-%d %H:%M:%S") future = (datetime.now(timezone.utc) + timedelta(days=18, hours=12)).strftime("%Y-%m-%d %H:%M:%S")
_set_trial_until(acct_id, future) _set_trial_until(acct_id, future)
resp = client.get("/_fragments/status") resp = client.get("/", follow_redirects=True)
assert resp.status_code == 200 assert resp.status_code == 200
html = resp.text html = resp.text
@@ -516,7 +518,8 @@ def test_afisaj_plan_si_zile_trial(client):
def test_afisaj_consum_lunar(client): def test_afisaj_consum_lunar(client):
"""US-006: cont free (fara trial) -> fragment status arata 'Gratuit · N/60 luna asta'.""" """US-006 + T-6 (5.16): cont free (fara trial) -> linia de plan din burger (pagina
completa) arata 'Gratuit · N/60 luna asta'. Consumul normal nu mai e rand in corp."""
acct_id, _ = _create_account_user("consumlun@test.com") acct_id, _ = _create_account_user("consumlun@test.com")
_login(client, "consumlun@test.com", "parolasecreta10") _login(client, "consumlun@test.com", "parolasecreta10")
@@ -525,7 +528,7 @@ def test_afisaj_consum_lunar(client):
# Insereaza 5 submissions sent luna asta # Insereaza 5 submissions sent luna asta
_insert_submissions_sent(acct_id, 5) _insert_submissions_sent(acct_id, 5)
resp = client.get("/_fragments/status") resp = client.get("/", follow_redirects=True)
assert resp.status_code == 200 assert resp.status_code == 200
html = resp.text html = resp.text
@@ -584,7 +587,8 @@ def test_copy_pluralizare_zi_zile(client):
future_18 = (datetime.now(timezone.utc) + timedelta(days=18, hours=12)).strftime("%Y-%m-%d %H:%M:%S") future_18 = (datetime.now(timezone.utc) + timedelta(days=18, hours=12)).strftime("%Y-%m-%d %H:%M:%S")
_set_trial_until(acct_id, future_18) _set_trial_until(acct_id, future_18)
resp = client.get("/_fragments/status") # T-6 (5.16): linia de plan (cu pluralizarea zilelor) traieste in burger pe pagina completa.
resp = client.get("/", follow_redirects=True)
assert resp.status_code == 200 assert resp.status_code == 200
html = resp.text html = resp.text
assert "18 zile" in html, f"'18 zile' lipseste. HTML: {html[:800]}" assert "18 zile" in html, f"'18 zile' lipseste. HTML: {html[:800]}"
@@ -596,7 +600,7 @@ def test_copy_pluralizare_zi_zile(client):
future_1 = (datetime.now(timezone.utc) + timedelta(days=1, hours=12)).strftime("%Y-%m-%d %H:%M:%S") future_1 = (datetime.now(timezone.utc) + timedelta(days=1, hours=12)).strftime("%Y-%m-%d %H:%M:%S")
_set_trial_until(acct_id, future_1) _set_trial_until(acct_id, future_1)
resp = client.get("/_fragments/status") resp = client.get("/", follow_redirects=True)
assert resp.status_code == 200 assert resp.status_code == 200
html = resp.text html = resp.text
assert "1 zi" in html, f"'1 zi' (singular) lipseste la o zi ramasa. HTML: {html[:800]}" assert "1 zi" in html, f"'1 zi' (singular) lipseste la o zi ramasa. HTML: {html[:800]}"

View File

@@ -107,8 +107,9 @@ def test_submissions_coloane_umane(client):
assert "B777ZZZ" in html, "Nr inmatriculare din payload lipseste" assert "B777ZZZ" in html, "Nr inmatriculare din payload lipseste"
assert "Reparatie frane" in html, "Operatia din payload lipseste" assert "Reparatie frane" in html, "Operatia din payload lipseste"
# Nr. prezentare RAR accesibil pe linia meta discreta # 5.16: #id_prezentare nu mai e pe rand (randul are MAX 2 linii) — detaliul complet
assert "68516" in html, "Nr. prezentare RAR lipseste din linia meta" # (inclusiv nr. prezentare RAR) traieste in modalul de detaliu.
assert "68516" not in html, "Nr. prezentare RAR nu trebuie sa mai apara pe randul slim"
def test_tab_eticheta_trimiteri(client): def test_tab_eticheta_trimiteri(client):
@@ -426,9 +427,9 @@ def test_detaliu_trimitere_404_cross_account(client):
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def test_rand_slim_vin_operatie_pill(client): def test_rand_slim_vin_operatie_pill(client):
"""US-004: fiecare rand slim afiseaza VIN scurt in .slim-vin, operatie+ora in """5.16: fiecare rand slim are 2 linii — L1 placuta (nr. inmatriculare) in .slim-vin,
.slim-meta si un pill de stare cu clasa stare_css si eticheta stare_scurt. L2 cod RAR · operatie · data in .slim-meta, plus un pill de stare cu clasa stare_css
Lista e inconjurata de .lista-trimiteri-slim. si eticheta stare_scurt. Lista e inconjurata de .lista-trimiteri-slim.
""" """
acct = _create_account_user("slim1@test.com") acct = _create_account_user("slim1@test.com")
_insert_submission(acct, "sent", id_prezentare=80001) _insert_submission(acct, "sent", id_prezentare=80001)
@@ -442,14 +443,16 @@ def test_rand_slim_vin_operatie_pill(client):
assert "lista-trimiteri-slim" in html, "lista-trimiteri-slim lipseste din raspuns" assert "lista-trimiteri-slim" in html, "lista-trimiteri-slim lipseste din raspuns"
assert "trimitere-slim" in html, "trimitere-slim lipseste din raspuns" assert "trimitere-slim" in html, "trimitere-slim lipseste din raspuns"
# VIN scurt in clasa slim-vin (mono, linia 1) # L1: placuta (identificator primar) in clasa slim-vin
assert "slim-vin" in html, "slim-vin lipseste — linia 1 VIN mono" assert "slim-vin" in html, "slim-vin lipseste — linia 1 placuta"
assert "B777ZZZ" in html, "placuta (nr. inmatriculare) lipseste de pe rand"
# Linia 2 muted (operatie + ora/data) # L2: cod RAR · operatie · data (slim-meta / slim-rand2)
assert "slim-meta" in html, "slim-meta lipseste — linia 2 muted" assert "slim-meta" in html, "slim-meta lipseste — linia 2"
assert "slim-rand2" in html, "slim-rand2 lipseste — linia 2 (cod RAR · operatie · data)"
# VIN scurt randat (WVWZZZ1JZXW000777 -> …000777) # VIN integral nu mai e pe rand (5.16) — traieste in modalul de detaliu.
assert "000777" in html, "VIN scurt (ultimele 6 cifre) lipseste" assert "000777" not in html, "VIN scurt nu mai trebuie randat pe randul slim (2 linii)"
# Pill de stare: clasa CSS + eticheta scurta # Pill de stare: clasa CSS + eticheta scurta
assert "s-sent" in html, "clasa pill s-sent lipseste" assert "s-sent" in html, "clasa pill s-sent lipseste"

View File

@@ -81,12 +81,13 @@ def client(monkeypatch):
get_settings.cache_clear() get_settings.cache_clear()
def test_vin_pe_rand_separat_sub_nr(client): def test_placuta_pe_rand_identificator_primar(client):
"""VIN-ul apare intr-un element block-level cu clasa slim-vin (PRD 5.15 US-004). """Placuta (nr. inmatriculare) e identificatorul PRIMAR, linia 1 a randului slim
(5.16): in <div class="slim-vin"> (block-level, prominent).
PRD 5.10 (US-005): VIN era <div class="muted"> sub nr in coloana Vehicul. PRD 5.15 (US-004): VIN era identificatorul primar pe linia 1.
PRD 5.15 (US-004): VIN e acum identificatorul PRINCIPAL, linia 1 a randului slim, 5.16 (directiva user): operatorul scaneaza placuta de pe comanda, nu VIN-ul de 17
in <div class="slim-vin"> (mono, prominent, block-level). NU mai e muted. caractere — placuta devine linia 1, VIN integral se muta in modalul de detaliu.
""" """
acct = _create_account_user("vin_layout@test.com") acct = _create_account_user("vin_layout@test.com")
_ins(acct, vin="WVWZZZ1JZXW000001", nr="B123XYZ") _ins(acct, vin="WVWZZZ1JZXW000001", nr="B123XYZ")
@@ -96,46 +97,51 @@ def test_vin_pe_rand_separat_sub_nr(client):
assert resp.status_code == 200 assert resp.status_code == 200
html = resp.text html = resp.text
# VIN trunchiat trebuie sa apara in HTML # Placuta trebuie sa apara in HTML
assert "000001" in html, "VIN-ul trunchiat trebuie sa apara in lista slim" assert "B123XYZ" in html, "placuta (nr. inmatriculare) trebuie sa apara in lista slim"
# VIN e intr-un element block-level (div cu clasa slim-vin) # Placuta e intr-un element block-level (div cu clasa slim-vin)
# Pattern: <div class="slim-vin">...000001...</div> plac = "B123XYZ"
vin_fragment = "000001"
found_slim_vin = re.search( found_slim_vin = re.search(
rf'<div[^>]*class="slim-vin[^"]*"[^>]*>[^<]*{re.escape(vin_fragment)}[^<]*</div>', rf'<div[^>]*class="slim-vin[^"]*"[^>]*>[^<]*{re.escape(plac)}[^<]*</div>',
html, html,
) )
assert found_slim_vin, ( assert found_slim_vin, (
f"VIN '{vin_fragment}' trebuie sa fie in <div class=\"slim-vin\"> (block-level, " f"placuta '{plac}' trebuie sa fie in <div class=\"slim-vin\"> (linia 1 a "
f"mono, linia 1 a randului slim). HTML gasit: " f"randului slim). HTML gasit: "
+ html[max(0, html.find(vin_fragment) - 80):html.find(vin_fragment) + 80] + html[max(0, html.find(plac) - 80):html.find(plac) + 80]
) )
# VIN integral NU mai e pe rand (max 2 linii) — traieste in modalul de detaliu.
assert "000001" not in html, "VIN-ul nu mai trebuie randat pe randul slim (5.16)"
def test_vin_lipsa_nu_genereaza_rand_gol(client):
"""Cand VIN-ul lipseste (sau e EMPTY=''), slim-vin nu afiseaza '' izolat. def test_placuta_lipsa_nu_genereaza_rand_gol(client):
Fallback: slim-vin afiseaza vehicul_nr (nr. inmatriculare) cu clasa muted. """Cand placuta SI VIN-ul lipsesc, slim-vin nu afiseaza '' izolat ca identificator.
(PRD 5.15 US-004: slim-vin are garda vin != '') Fallback (5.16): VIN scurt daca exista, altfel mesaj neutru ('fara numar') — niciodata
un em-dash singur ca identificator primar.
""" """
acct = _create_account_user("vin_gol@test.com") acct = _create_account_user("vin_gol@test.com")
sid = _ins(acct, vin="", nr="B999TST") # VIN gol -> vin_scurt='—' # Placuta prezenta -> e identificatorul primar pe linia 1.
sid1 = _ins(acct, vin="", nr="B999TST")
# Placuta SI VIN absente -> fallback 'fara numar' (nu '—' izolat).
sid2 = _ins(acct, vin="", nr="")
_login(client, "vin_gol@test.com") _login(client, "vin_gol@test.com")
resp = client.get("/_fragments/submissions") resp = client.get("/_fragments/submissions")
assert resp.status_code == 200 assert resp.status_code == 200
html = resp.text html = resp.text
# Randul trebuie sa existe # Ambele randuri exista
assert f'id="trimitere-row-{sid}"' in html assert f'id="trimitere-row-{sid1}"' in html
assert f'id="trimitere-row-{sid2}"' in html
# slim-vin NU trebuie sa contina '—' izolat (VIN lipsa -> fallback vehicul_nr) # Placuta vizibila cand exista
slim_vin_match = re.search(r'<div[^>]*class="slim-vin[^"]*"[^>]*>([^<]*)</div>', html) assert "B999TST" in html, "placuta (nr. inmatriculare) lipseste de pe rand"
assert slim_vin_match, "slim-vin lipseste din randul cu VIN gol"
slim_vin_content = slim_vin_match.group(1).strip() # Niciun slim-vin nu contine '—' izolat
assert slim_vin_content != "", ( for m in re.finditer(r'<div[^>]*class="slim-vin[^"]*"[^>]*>([^<]*)</div>', html):
"slim-vin afiseaza '' izolat cand VIN lipseste — " assert m.group(1).strip() != "", "slim-vin afiseaza '' izolat ca identificator"
"trebuie sa afiseze vehicul_nr ca fallback"
) # Fallback neutru cand placuta + VIN lipsesc
# Fallback: nr inmatriculare vizibil assert "fara numar" in html, "fallback 'fara numar' lipseste cand placuta+VIN absente"
assert "B999TST" in html, "Nr inmatriculare (fallback) lipseste cand VIN e gol"