fix(5.11): tabel trimiteri stabil — bug status=None, pills in bara de filtre, nudge "Date noi" in loc de poll 15s, logo ROMFAST marit

- Fix bug: campul hidden de filtru randa literal "None" (status_filtru None +
  Jinja default('')) -> poll-ul trimitea status=None -> tabel gol. status or "".
- Pills de stare mutate din bara de status in bara de filtre (filtreazaStare scrie
  campul hidden + re-trimite form-ul; filtrul persista la reincarcari). Re-randate
  OOB cu contoare proaspete la fiecare reincarcare a tabelului.
- Polling redesign: tabelul nu se mai reincarca singur (fara every 15s). Poller usor
  JSON (/_fragments/trimiteri-versiune) detecteaza schimbari -> nudge "Date noi —
  Reincarca". Reincarcarea (nudge / actiune) pastreaza filtrul+pagina. Scroll/selectia
  nu se mai pierd. Poll-guard eliminat (nu mai exista poll periodic de pauzat).
- Logo ROMFAST 32px -> 60px (ca pe romfast.ro), header min-height 92px, 44px pe mobil.

Regresie: 896 passed, 1 deselected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-06-25 21:13:42 +00:00
parent 074b6e7c8a
commit f05fe5b221
10 changed files with 224 additions and 199 deletions

View File

@@ -20,7 +20,7 @@ from pathlib import Path
from typing import Any
from fastapi import APIRouter, File, Form, HTTPException, Request, UploadFile
from fastapi.responses import HTMLResponse
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.templating import Jinja2Templates
from .. import __version__
@@ -113,6 +113,18 @@ def _status_counts(conn, account_id: int) -> dict[str, int]:
return {r["status"]: int(r["n"]) for r in rows}
def _trimiteri_versiune(conn, account_id: int) -> str:
"""Semnatura ieftina a starii trimiterilor contului: numar randuri + cel mai recent
updated_at. Se schimba la orice insert/update/delete -> nudge-ul "Date noi" o compara
fara a re-randa tabelul."""
row = conn.execute(
"SELECT COUNT(*) AS n, COALESCE(MAX(updated_at), '') AS m FROM submissions "
"WHERE (account_id = ? OR (? = 1 AND account_id IS NULL))",
(account_id, account_id),
).fetchone()
return f"{row['n']}:{row['m']}"
def _account_active(conn, account_id: int) -> bool:
"""True daca contul e activ (sau legacy cu NULL/absent active)."""
row = conn.execute("SELECT active FROM accounts WHERE id=?", (account_id,)).fetchone()
@@ -196,6 +208,10 @@ def _get_acasa_context(request: Request, conn, account_id: int) -> dict:
"are_trimiteri": are_trimiteri,
"are_cheie_folosita": are_cheie_folosita,
"blocate_total": blocate_total,
# Pill-uri de filtrare a starii, randate in bara de filtre (nu in bara de status).
"pills_categorii": _pills_categorii(counts),
# Semnatura datelor: nudge-ul "Date noi" o compara la fiecare poll usor.
"versiune_trimiteri": _trimiteri_versiune(conn, account_id),
# US-002: Acasa include caseta de upload -> are nevoie de csrf_token
"csrf_token": get_csrf_token(request),
}
@@ -212,7 +228,9 @@ def _render_panel_acasa(request: Request, conn=None, account_id: int = 1, status
{"request": request, "csrf_token": get_csrf_token(request)}
)
ctx = _get_acasa_context(request, conn, account_id)
ctx["status_filtru"] = status
# `status or ""`: campul hidden de filtru ar randa literal "None" cu un None Python
# (Jinja `default('')` inlocuieste doar undefined), trimitand status=None la poll.
ctx["status_filtru"] = status or ""
return templates.get_template("_acasa.html").render(ctx)
@@ -621,6 +639,19 @@ def fragment_status(request: Request) -> HTMLResponse:
conn.close()
@router.get("/_fragments/trimiteri-versiune", response_class=JSONResponse)
def fragment_trimiteri_versiune(request: Request) -> JSONResponse:
"""Semnatura curenta a trimiterilor contului (JSON usor). Pollerul "Date noi" o
compara cu versiunea cu care s-a randat tabelul; daca difera, arata nudge-ul de
reincarcare — tabelul nu se mai schimba singur."""
account_id = require_login(request)
conn = get_connection()
try:
return JSONResponse({"v": _trimiteri_versiune(conn, account_id)})
finally:
conn.close()
def _iso_date_prefix(value: object) -> str | None:
"""Intoarce primele 10 caractere (YYYY-MM-DD) daca incep cu o data ISO valida, altfel None.
@@ -795,6 +826,10 @@ def fragment_submissions(
"f_vehicul": vehicul_q or "",
"f_data_de": data_de or "",
"f_data_pana": data_pana or "",
# Pill-uri (OOB) + stare activa + versiune pentru nudge-ul "Date noi".
"pills_categorii": _pills_categorii(_status_counts(conn, account_id)),
"status_filtru": status or "",
"versiune_trimiteri": _trimiteri_versiune(conn, account_id),
})
finally:
conn.close()
@@ -823,6 +858,9 @@ def _render_submissions(request: Request, conn, account_id: int) -> HTMLResponse
"rows": view,
"filtru_activ": False,
"csrf_token": get_csrf_token(request),
"pills_categorii": _pills_categorii(_status_counts(conn, account_id)),
"status_filtru": "",
"versiune_trimiteri": _trimiteri_versiune(conn, account_id),
})

View File

@@ -21,19 +21,16 @@
</span>
</div>
<!-- Filtre (US-009): reincarca tabelul; poll-ul re-trimite filtrul curent prin hx-include -->
<!-- Bara de filtre: vehicul/data + pill-uri de stare pe acelasi rand. Pill-urile scriu
campul hidden status si re-trimit form-ul (filtreazaStare) -> filtrul persista la reincarcari. -->
<form id="filtre-trimiteri"
hx-get="/_fragments/submissions"
hx-target="#submissions-wrap"
hx-swap="innerHTML"
hx-trigger="submit, change, keyup delay:400ms from:input[name='vehicul']"
style="display:flex; gap:10px; flex-wrap:wrap; align-items:flex-end; margin-bottom:12px;">
{# US-003 (PRD 5.10): dropdown status eliminat — inlocuit cu pill-uri in bara de status.
Filtrul de stare vine de la pill-uri (/_fragments/submissions?status=X direct).
Camp hidden permite reset stare la submit manual din form (Filtreaza). #}
<input type="hidden" name="status" value="{{ status_filtru | default('') }}">
{# US-004 (PRD 5.10): pagina curenta — actualizata prin OOB swap din _submissions.html.
Poll-ul (hx-include="#filtre-trimiteri") include automat pagina curenta (L2 PRD). #}
style="display:flex; gap:10px 14px; flex-wrap:wrap; align-items:flex-end; margin-bottom:12px;">
<input type="hidden" id="f-status" name="status" value="{{ status_filtru | default('', true) }}">
{# Pagina curenta — actualizata prin OOB swap din _submissions.html; inclusa la reincarcari. #}
<input type="hidden" id="f-page" name="page" value="1">
<div>
<label for="f-vehicul" class="muted" style="display:block; font-size:12px;">Vehicul (nr/VIN)</label>
@@ -48,12 +45,24 @@
<input id="f-data-pana" type="date" name="data_pana">
</div>
<button type="submit">Filtreaza</button>
{# Pill-uri de stare pe acelasi rand cu filtrele; re-randate prin OOB la reincarcarea tabelului. #}
<span id="pills-categorii" class="pills-categorii" style="margin-left:auto;">
{% include '_pills.html' %}
</span>
</form>
<!-- Poll aliniat la 15s ca status-ul (M5: nu doua timere perpetue pe pagina mereu deschisa) -->
<!-- Nudge "Date noi": tabelul nu se reimprospateaza singur; bannerul apare doar cand
pollerul usor detecteaza schimbari, iar utilizatorul reincarca cand vrea. -->
<div id="nudge-trimiteri" hidden role="status" aria-live="polite">
<span>Sunt trimiteri actualizate.</span>
<button type="button" onclick="reincarcaTrimiteri()">Reincarca</button>
</div>
<!-- Tabelul se reincarca DOAR la: incarcarea paginii, actiunile tale (trimiteriChanged)
sau apasarea pe Reincarca (reincarcaTrimiteri). Fara poll periodic care sa-l reseteze. -->
<div id="submissions-wrap"
hx-get="/_fragments/submissions"
hx-trigger="load, every 15s, trimiteriChanged from:body"
hx-trigger="load, trimiteriChanged from:body, reincarcaTrimiteri"
hx-include="#filtre-trimiteri" hx-swap="innerHTML">
<div class="empty">se incarca…</div>
</div>

View File

@@ -0,0 +1,15 @@
{# Pill-uri de filtrare a starii, randate in bara de filtre (_coada.html) si re-randate
prin OOB la fiecare reincarcare a tabelului (_submissions.html). Stare activa =
status_filtru. "Toate" reseteaza filtrul; categoriile apar doar cand au n>0. #}
<button type="button" class="pill-cat pill-cat-reset" data-status=""
aria-pressed="{{ 'true' if not status_filtru else 'false' }}"
onclick="filtreazaStare(this, '')">Toate</button>
{% for pill in pills_categorii %}
<button type="button" class="pill-cat" data-status="{{ pill.status }}"
aria-pressed="{{ 'true' if status_filtru == pill.status else 'false' }}"
style="color:var({{ pill.color_var }}); border-color:var({{ pill.color_var }});"
onclick="filtreazaStare(this, '{{ pill.status }}')">
{{ pill.label }}
<span class="pill-cat-n" style="background:var({{ pill.color_var }});">{{ pill.n }}</span>
</button>
{% endfor %}

View File

@@ -1,15 +1,3 @@
<style>
/* Pill-uri categorii blocate (US-003 PRD 5.10)
Culoarea e injectata inline (color_var: --warn / --err) dupa DESIGN.md:
Lipsa cod = --warn (chihlimbar), Date incomplete + Eroare = --err (rosu).
Activ = fundal pe culoarea categoriei (NU accent albastru — S1/A5). */
.pill-cat { transition: background 0.15s, color 0.15s; }
.pill-cat:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* Activ: background = culoarea categoriei (currentColor = var(--err/--warn) din inline style),
text = var(--card) (contrast AA). NU accent albastru (S1/A5 DESIGN.md). */
.pill-cat[aria-pressed="true"] { background: currentColor !important; color: var(--card) !important; border-color: currentColor !important; }
.pill-cat[aria-pressed="true"] span { background: var(--card) !important; color: currentColor; }
</style>
<div id="status-bar" class="status-bar card"
hx-get="/_fragments/status"
hx-trigger="every 15s"
@@ -59,51 +47,6 @@
</span>
</div>
<!-- Pill-uri categorii blocate (US-003 PRD 5.10): inlocuiesc lista de ID-uri.
<button> reale cu aria-pressed, focalizabile, activare Enter/Space.
Inactiv = contur+text pe culoarea categoriei; activ = umplere pe culoarea categoriei.
Pill ascuns cand n=0 (lista pills_categorii filtreaza deja). -->
{% if pills_categorii %}
<div style="margin-top:12px; padding-top:10px; border-top:1px solid var(--line);">
<div style="display:flex; gap:8px; flex-wrap:wrap; align-items:center;">
<span style="font-size:12px; color:var(--muted);">Necesita atentie:</span>
{% for pill in pills_categorii %}
<button type="button"
class="pill-cat"
aria-pressed="false"
hx-get="/_fragments/submissions?status={{ pill.status }}"
hx-target="#submissions-wrap"
hx-swap="innerHTML"
onclick="(function(b){
var pressed=b.getAttribute('aria-pressed')==='true';
document.querySelectorAll('.pill-cat').forEach(function(x){x.setAttribute('aria-pressed','false');});
if(!pressed){b.setAttribute('aria-pressed','true');}
var s=document.getElementById('trimiteri-section');
if(s){s.scrollIntoView({behavior:'smooth'});}
})(this)"
style="display:inline-flex; align-items:center; gap:5px;
padding:3px 10px; border-radius:99px; font-size:12px; font-weight:600;
cursor:pointer; border:1.5px solid var({{ pill.color_var }}); color:var({{ pill.color_var }});
background:transparent; transition:background 0.15s, color 0.15s;">
{{ pill.label }}
<span style="font-size:11px; font-weight:700; background:var({{ pill.color_var }}); color:var(--card);
padding:0 5px; border-radius:99px; min-width:18px; text-align:center;">{{ pill.n }}</span>
</button>
{% endfor %}
{# Buton "Toate" — reseteaza filtrul de categorie #}
<button type="button"
class="pill-cat-reset"
aria-label="Arata toate trimiterile"
hx-get="/_fragments/submissions"
hx-target="#submissions-wrap"
hx-swap="innerHTML"
onclick="document.querySelectorAll('.pill-cat').forEach(function(x){x.setAttribute('aria-pressed','false');})"
style="padding:3px 10px; border-radius:99px; font-size:12px; cursor:pointer;
border:1px solid var(--line); background:transparent; color:var(--muted);">
Toate
</button>
</div>
</div>
{% endif %}
{# Pill-urile de stare s-au mutat in bara de filtre din sectiunea Trimiteri (_coada.html). #}
</div>

View File

@@ -5,6 +5,13 @@
#}
<input type="hidden" id="f-page" name="page" value="{{ page | default(1) }}" hx-swap-oob="true">
{# OOB: re-randeaza pill-urile de stare (in bara de filtre, in afara #submissions-wrap) cu
contoarele si starea activa proaspete la fiecare reincarcare a tabelului. #}
<span hx-swap-oob="innerHTML:#pills-categorii">{% include '_pills.html' %}</span>
{# Versiunea datelor cu care s-a randat tabelul; pollerul "Date noi" o compara. #}
<span id="trimiteri-versiune" data-v="{{ versiune_trimiteri | default('') }}" hidden></span>
{% if rows %}
{# US-011: form de stergere bulk. Selectia opereaza DOAR pe randuri blocate
(gestionabil); sent/sending/queued nu au checkbox (read-only). #}

View File

@@ -123,7 +123,7 @@
background:var(--bg); color:var(--ink); -webkit-font-smoothing:antialiased; }
/* US-012c (PRD 5.10): grila 3 coloane — stanga (logo ROMFAST) | centru (titlu+env) | dreapta (controale). */
header { padding:16px 24px; border-bottom:1px solid var(--line);
display:grid; grid-template-columns:1fr auto 1fr; align-items:center; gap:8px; }
display:grid; grid-template-columns:1fr auto 1fr; align-items:center; gap:8px; min-height:92px; }
.header-left { display:flex; align-items:center; }
.header-center { display:flex; flex-direction:column; align-items:center; text-align:center; }
.header-right { display:flex; align-items:center; justify-content:flex-end; gap:8px; }
@@ -131,7 +131,8 @@
32px inaltime — usor mai mare decat in header-center (28px) pentru vizibilitate ca brand anchor.
margin:0 — aliniat stanga, NU centrat (era `margin:3px auto 0` cand era sub titlu).
Logo transparent: ok pe dark/light/petrol fara filtre de culoare. */
.brand-logo { height:32px; width:auto; display:block; margin:0; }
/* Logo ROMFAST la dimensiunea de pe romfast.ro (~60px inaltime), aliniat stanga. */
.brand-logo { height:60px; width:auto; display:block; margin:0; }
/* Env badge mic sub titlu in header-center (US-012c): nu mai echilibreaza optic dreapta
(logo-ul face asta), ci identifica mediul langa titlu. Pastrat mic, color:var(--muted). */
.header-center .env { font-size:11px; margin-top:2px; }
@@ -149,6 +150,28 @@
th { color:var(--muted); font-weight:500; font-size:12px; text-transform:uppercase; letter-spacing:.04em; }
.empty { color:var(--muted); padding:24px; text-align:center; }
.pill { font-size:12px; padding:2px 8px; border-radius:99px; border:1px solid var(--line); }
/* Pill-uri de filtrare a starii (bara de filtre Trimiteri). Inactiv = contur+text pe
culoarea categoriei (injectata inline); activ = umplere pe acea culoare. */
.pills-categorii { display:inline-flex; gap:8px; flex-wrap:wrap; align-items:center; }
.pill-cat { display:inline-flex; align-items:center; gap:5px; padding:4px 11px; border-radius:99px;
font-size:12px; font-weight:600; cursor:pointer; background:transparent;
border:1.5px solid var(--line); color:var(--muted); min-height:30px;
transition:background .15s, color .15s; }
.pill-cat:hover { filter:brightness(1.1); }
.pill-cat:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
.pill-cat-n { font-size:11px; font-weight:700; color:var(--card); padding:0 5px;
border-radius:99px; min-width:18px; text-align:center; }
.pill-cat[aria-pressed="true"] { background:currentColor; color:var(--card); border-color:currentColor; }
.pill-cat[aria-pressed="true"] .pill-cat-n { background:var(--card) !important; color:currentColor; }
.pill-cat-reset[aria-pressed="true"] { background:var(--accent); color:#fff; border-color:var(--accent); }
/* Nudge "Date noi": apare doar cand pollerul usor detecteaza schimbari; tabelul nu se
schimba singur niciodata, utilizatorul reincarca cand vrea. */
#nudge-trimiteri { display:flex; align-items:center; gap:10px; flex-wrap:wrap; margin:0 0 12px;
padding:8px 12px; border-radius:8px; font-size:13px;
border:1px solid var(--accent);
background:color-mix(in srgb, var(--accent) 12%, var(--card)); }
#nudge-trimiteri[hidden] { display:none; }
#nudge-trimiteri button { font-size:13px; padding:5px 12px; min-height:32px; }
.s-queued{color:var(--accent);} .s-sending{color:var(--warn);} .s-sent{color:var(--ok);}
.s-error,.s-needs_data,.s-needs_mapping{color:var(--err);}
.s-ok{color:var(--ok);}
@@ -349,7 +372,8 @@
/* Header + nav colapsate: pe mobil trece de la grid la flex wrap.
Randul 1: [logo ROMFAST stanga] [controale dreapta] (margin-left:auto pe .header-right).
Randul 2: [titlu + env mic centrat, full-width]. Fara scroll orizontal, tinte >=44px. */
header { display:flex; flex-wrap:wrap; padding:12px 16px; gap:8px; align-items:center; }
header { display:flex; flex-wrap:wrap; padding:12px 16px; gap:8px; align-items:center; min-height:0; }
.brand-logo { height:44px; }
.header-left { order:0; flex:0 0 auto; }
.header-center { order:2; width:100%; text-align:center; }
.header-right { order:1; margin-left:auto; flex:0 0 auto; }
@@ -789,45 +813,49 @@
})();
</script>
<script>
// Poll-guard (PRD 5.9 US-005, R6). Inlocuieste vechea pauza pe „rand expandat" (5.8):
// randul-sibling de detaliu nu mai exista (US-003 l-a mutat in modalul global, care
// traieste in afara #submissions-wrap -> un swap de poll nu-l atinge). Aici oprim
// poll-ul de 15s de a REINCARCA lista cat timp (a) modalul e deschis SAU (b) exista
// cel putin un checkbox de bulk bifat — altfel modalul s-ar reseta / bifele s-ar sterge.
//
// CRITIC (F5): blocam DOAR trigger-ul periodic. In htmx `load`/`every 15s` declanseaza
// requestul FARA `triggeringEvent`; `trimiteriChanged` (HX-Trigger dupa corectie/stergere)
// si submit-ul/filtrul AU `triggeringEvent` -> TREC MEREU. Asa evitam blocajul permanent:
// daca randul bifat paraseste filtrul, pauza nu ramane lipita (pauza e legata strict de
// trigger-ul periodic, nu de o stare „sticky"). Anularea unui `htmx:beforeRequest` NU
// opreste timer-ul htmx (se reprogrameaza singur) -> poll-ul reia automat la urmatorul
// tic cand ambele conditii dispar; nu se pierde scroll, focus sau selectia de bife.
// Filtrare stare prin pill-uri + reincarcare manuala a tabelului. Tabelul NU se mai
// schimba singur (fara poll periodic pe #submissions-wrap): un poller usor verifica
// doar versiunea datelor si arata nudge-ul "Date noi" cand difera. Reincarcarea
// (pill, nudge sau actiune) trece prin form -> pastreaza filtrul/pagina curenta.
(function() {
function modalDeschis() {
var o = document.getElementById('modal-detaliu');
return !!(o && !o.hidden);
// Pill de stare: scrie campul hidden, reseteaza pagina la 1 si re-trimite filtrul.
window.filtreazaStare = function(btn, status) {
var form = document.getElementById('filtre-trimiteri');
if (!form) return;
var hs = document.getElementById('f-status'); if (hs) hs.value = status || '';
var hp = document.getElementById('f-page'); if (hp) hp.value = '1';
document.querySelectorAll('#pills-categorii .pill-cat').forEach(function(b) {
b.setAttribute('aria-pressed', 'false');
});
if (btn) btn.setAttribute('aria-pressed', 'true');
if (form.requestSubmit) form.requestSubmit(); else form.submit();
};
// Reincarca tabelul pastrand filtrul curent (hx-include #filtre-trimiteri) si ascunde nudge-ul.
window.reincarcaTrimiteri = function() {
var n = document.getElementById('nudge-trimiteri'); if (n) n.hidden = true;
if (window.htmx) htmx.trigger('#submissions-wrap', 'reincarcaTrimiteri');
};
// Poller "Date noi": compara versiunea datelor cu cea cu care s-a randat tabelul.
// Daca difera, arata nudge-ul; daca nu, nu atinge nimic. JSON usor, fara re-render.
var INTERVAL = 20000;
function versiuneCurenta() {
var e = document.getElementById('trimiteri-versiune');
return e ? e.getAttribute('data-v') : null;
}
function existaBifa() {
return !!document.querySelector('#submissions-wrap input[name="submission_id"]:checked');
function verifica() {
if (versiuneCurenta() === null) return; // tabelul nu e pe ecran (alt tab)
var nudge = document.getElementById('nudge-trimiteri');
if (!nudge || !nudge.hidden) return; // deja afisat -> nu re-cere
fetch('/_fragments/trimiteri-versiune', { headers: { 'X-Requested-With': 'fetch' } })
.then(function(r) { return r.ok ? r.json() : null; })
.then(function(d) {
if (!d) return;
if (d.v !== versiuneCurenta()) nudge.hidden = false;
})
.catch(function() {});
}
document.body.addEventListener('htmx:beforeRequest', function(evt) {
var d = evt.detail || {};
if (!d.elt || d.elt.id !== 'submissions-wrap') return; // doar poll-ul listei
var rc = d.requestConfig || {};
if (rc.triggeringEvent) return; // trimiteriChanged / filtru: TREC MEREU
if (modalDeschis() || existaBifa()) evt.preventDefault(); // pauza scopata pe periodic
});
// Resume pe checkbox `change`->gol: delegare pe body ca sa prinda si checkbox-urile
// randate dupa swap. Cand modalul e inchis si nu mai exista nicio bifa, fortam un
// refresh imediat (nu mai asteptam ticul de 15s) prin `trimiteriChanged from:body`,
// care pastreaza filtrul curent (hx-include #filtre-trimiteri) si trece de guard.
document.body.addEventListener('change', function(evt) {
var t = evt.target;
if (!(t && t.name === 'submission_id')) return;
if (!modalDeschis() && !existaBifa() && window.htmx) {
htmx.trigger(document.body, 'trimiteriChanged');
}
});
setInterval(verifica, INTERVAL);
})();
</script>
</body>

View File

@@ -128,10 +128,19 @@ def test_badge_trimiteri_scoped_pe_acasa(client):
assert "2" in html[idx:idx + 400]
def test_trimiteri_poll_aliniat_15s(client):
"""Poll-ul de trimiteri e aliniat la 15s (anti dublu-poll M5), nu 10s."""
def test_trimiteri_fara_poll_periodic_pe_tabel(client):
"""Tabelul de trimiteri NU se mai reimprospateaza periodic: #submissions-wrap se
incarca la load / actiunile utilizatorului / Reincarca (nudge), fara `every Ns`.
Reimprospatarea live se face prin nudge-ul "Date noi" + endpointul de versiune."""
_seed_submission("sent")
r = client.get("/?tab=acasa")
html = r.text
assert "every 15s" in html
assert "every 10s" not in html
# Trigger-ul tabelului nu contine poll periodic.
wrap = html[html.find('id="submissions-wrap"'):]
wrap = wrap[:wrap.find(">") + 1]
assert "every" not in wrap, f"tabelul nu trebuie sa aiba poll periodic: {wrap}"
assert "reincarcaTrimiteri" in wrap
assert "trimiteriChanged" in wrap
# Mecanismul de nudge exista (banner + endpoint versiune).
assert 'id="nudge-trimiteri"' in html
assert "/_fragments/trimiteri-versiune" in html

View File

@@ -161,58 +161,48 @@ def test_modal_hookuri_js_prezente(client):
assert "window.inchideDetaliu" in js
# --- PRD 5.9 US-005 (R6): poll-guard ---------------------------------------
# Modalul + selectia trebuie sa supravietuiasca poll-ului de 15s. Logica e JS in
# base.html: testam la nivel de markup/handler ca guard-ul exista si distinge corect
# sursa trigger-ului (periodic vs trimiteriChanged/filtru). Comportamentul runtime
# efectiv (anularea propriu-zisa) e validat E2E (requiresBrowserCheck) — aici asertam
# codul/atributele care il implementeaza.
# --- Tabelul nu se reincarca singur: modalul + selectia sunt sigure ---------
# Tabelul (#submissions-wrap) nu mai are poll periodic; se reincarca DOAR la load,
# la actiunile utilizatorului (trimiteriChanged) sau la apasarea pe Reincarca (nudge).
# Asa, modalul deschis si bifele de bulk nu pot fi sterse de un timer.
def test_poll_pauzat_cat_modal_deschis(client):
"""Guard-ul de poll exista si, cat modalul de detaliu e deschis, anuleaza
reincarcarea periodica a listei (#submissions-wrap), nu pe restul."""
_create_account_user("poll1@test.com")
def test_tabel_fara_poll_periodic(client):
"""#submissions-wrap nu are trigger periodic (`every Ns`) — niciun timer nu poate
reseta modalul deschis sau selectia de bulk in timpul interactiunii."""
acct = _create_account_user("poll1@test.com")
_login(client, "poll1@test.com")
js = client.get("/?tab=acasa").text
_insert_submission(acct)
html = client.get("/?tab=acasa").text
# Guard scopat la poll-ul listei, declansat pe htmx:beforeRequest.
assert "htmx:beforeRequest" in js
assert "d.elt.id !== 'submissions-wrap'" in js, "guard-ul trebuie scopat la #submissions-wrap"
# Conditia (a): modal deschis -> pauza (preventDefault).
assert "modalDeschis" in js
assert "modal-detaliu" in js and "hidden" in js
assert "evt.preventDefault()" in js, "pauza scopata se face prin preventDefault"
assert 'id="submissions-wrap"' in html
wrap = html[html.find('id="submissions-wrap"'):]
wrap = wrap[:wrap.find(">") + 1]
assert "every" not in wrap, f"tabelul nu trebuie sa aiba poll periodic: {wrap}"
def test_poll_pauzat_cat_exista_bifa(client):
"""Conditia (b): macar un checkbox de bulk bifat -> poll-ul periodic e pus pe
pauza. Resume pe checkbox `change` prin delegare pe body (prinde si bifele
randate dupa swap)."""
_create_account_user("poll2@test.com")
def test_nudge_date_noi_in_loc_de_poll(client):
"""Reimprospatarea live se face prin nudge-ul 'Date noi' (poller usor de versiune)
care NU atinge tabelul; utilizatorul reincarca explicit cand vrea."""
acct = _create_account_user("poll2@test.com")
_login(client, "poll2@test.com")
js = client.get("/?tab=acasa").text
_insert_submission(acct)
html = client.get("/?tab=acasa").text
# Detecteaza bifa de bulk in interiorul #submissions-wrap.
assert "existaBifa" in js
assert 'input[name="submission_id"]:checked' in js
# Resume: delegare pe body pe evenimentul `change` al checkbox-ului de bulk.
assert "addEventListener('change'" in js
assert "t.name === 'submission_id'" in js
assert 'id="nudge-trimiteri"' in html, "bannerul nudge 'Date noi' trebuie sa existe"
assert "/_fragments/trimiteri-versiune" in html, "pollerul de versiune trebuie configurat"
assert "reincarcaTrimiteri" in html, "reincarcarea manuala (Reincarca) trebuie expusa"
def test_trimiteriChanged_inca_reincarca_cu_bifa(client):
"""R6 (F5): guard-ul NU anuleaza request-urile cu `triggeringEvent`
(trimiteriChanged / submit filtru) — acelea TREC MEREU, ca pauza sa nu ramana
lipita permanent daca randul bifat paraseste filtrul."""
_create_account_user("poll3@test.com")
def test_trimiteriChanged_inca_reincarca(client):
"""Actiunile utilizatorului (corectie / stergere) reincarca tabelul prin canalul
`trimiteriChanged`, pastrand filtrul curent (hx-include #filtre-trimiteri)."""
acct = _create_account_user("poll3@test.com")
_login(client, "poll3@test.com")
js = client.get("/?tab=acasa").text
_insert_submission(acct)
html = client.get("/?tab=acasa").text
# Numai trigger-ul periodic (fara triggeringEvent) e candidat la pauza;
# orice request cu triggeringEvent iese devreme din guard.
assert "triggeringEvent" in js
assert "rc.triggeringEvent) return" in js, \
"request-urile cu triggeringEvent (trimiteriChanged/filtru) trebuie sa treaca mereu"
# Resume explicit reutilizeaza acelasi canal `trimiteriChanged` (pastreaza filtrul).
assert "trimiteriChanged" in js
wrap = html[html.find('id="submissions-wrap"'):]
wrap = wrap[:wrap.find(">") + 1]
assert "trimiteriChanged from:body" in wrap, "tabelul trebuie sa reincarce pe trimiteriChanged"
assert 'hx-include="#filtre-trimiteri"' in wrap, "reincarcarea trebuie sa pastreze filtrul"

View File

@@ -84,25 +84,24 @@ def test_pill_per_categorie_cu_numar(client):
_ins(acct, status="sent", vin="WVIN_SE1_001", nr="BSE1")
_login(client, "pill1@test.com")
resp = client.get("/_fragments/status")
# Pill-urile traiesc in bara de filtre din sectiunea Trimiteri.
resp = client.get("/?tab=acasa")
assert resp.status_code == 200
body = resp.text
# Pill-urile sunt elemente <button> (nu <span onclick>)
# Pill-urile sunt elemente <button>
assert "<button" in body, "Pill-urile trebuie sa fie elemente <button>"
# Fiecare categorie problemativa apare ca pill
assert "needs_data" in body, "Pill needs_data trebuie sa apara"
assert "needs_mapping" in body, "Pill needs_mapping trebuie sa apara"
assert "error" in body, "Pill error trebuie sa apara (hx-get sau text)"
assert 'data-status="needs_data"' in body, "Pill needs_data trebuie sa apara"
assert 'data-status="needs_mapping"' in body, "Pill needs_mapping trebuie sa apara"
assert 'data-status="error"' in body, "Pill error trebuie sa apara"
# Contoarele sunt afisate in pill-uri
assert ">2<" in body or "2<" in body, "Contorul 2 pt needs_data trebuie vizibil in pill"
# Starea 'sent' nu produce pill (nu e categorie de problema)
# (nu exista un pill cu status=sent in bara de status)
pill_sent_count = body.count("status=sent")
assert pill_sent_count == 0, "Nu trebuie pill pentru sent in bara de status"
assert 'data-status="sent"' not in body, "Nu trebuie pill pentru sent"
def test_pill_click_seteaza_status(client):
@@ -113,21 +112,21 @@ def test_pill_click_seteaza_status(client):
_ins(acct, status="error", vin="WVIN_ER2_001", nr="BER_P2a")
_login(client, "pill2@test.com")
resp = client.get("/_fragments/status")
resp = client.get("/?tab=acasa")
assert resp.status_code == 200
body = resp.text
# Fiecare pill are atribut hx-get cu parametrul status corespunzator
assert "status=needs_data" in body, "Pill needs_data trebuie sa aiba ?status=needs_data in hx-get"
assert "status=needs_mapping" in body, "Pill needs_mapping trebuie sa aiba ?status=needs_mapping in hx-get"
assert "status=error" in body, "Pill error trebuie sa aiba ?status=error in hx-get"
# Fiecare pill scrie campul de filtru prin filtreazaStare(this, 'X')
assert "filtreazaStare(this, 'needs_data')" in body, "Pill needs_data trebuie sa apeleze filtreazaStare cu needs_data"
assert "filtreazaStare(this, 'needs_mapping')" in body, "Pill needs_mapping trebuie sa apeleze filtreazaStare cu needs_mapping"
assert "filtreazaStare(this, 'error')" in body, "Pill error trebuie sa apeleze filtreazaStare cu error"
# Pill-urile au aria-pressed pentru accesibilitate (WCAG)
assert "aria-pressed" in body, "Pill-urile trebuie sa aiba atribut aria-pressed"
# Target-ul este tabelul de trimiteri
assert "submissions-wrap" in body or "_fragments/submissions" in body, (
"Pill-urile trebuie sa targeteze #submissions-wrap sau sa apeleze /_fragments/submissions"
# Filtrarea trece prin form-ul care targeteaza tabelul de trimiteri
assert "submissions-wrap" in body and "_fragments/submissions" in body, (
"Form-ul de filtre trebuie sa targeteze #submissions-wrap prin /_fragments/submissions"
)
@@ -161,7 +160,7 @@ def test_pill_needs_mapping_culoare_warn(client):
_ins(acct, status="needs_mapping", vin="WVIN_NM_WARN0001", nr="BNMW1")
_login(client, "warn@test.com")
resp = client.get("/_fragments/status")
resp = client.get("/?tab=acasa")
assert resp.status_code == 200
body = resp.text
@@ -169,4 +168,4 @@ def test_pill_needs_mapping_culoare_warn(client):
assert "var(--warn)" in body, (
"Pill needs_mapping trebuie sa foloseasca var(--warn) conform DESIGN.md §Componente"
)
assert "status=needs_mapping" in body, "Pill needs_mapping trebuie sa fie prezent in bara"
assert 'data-status="needs_mapping"' in body, "Pill needs_mapping trebuie sa fie prezent in bara de filtre"

View File

@@ -133,26 +133,16 @@ def test_status_blocate_defalcare(client):
_insert_submission("needs_data", acct_id)
_insert_submission("error", acct_id)
resp = client.get("/_fragments/status")
# Pill-urile s-au mutat in bara de filtre din sectiunea Trimiteri (nu in bara de status).
resp = client.get("/?tab=acasa")
assert resp.status_code == 200
html = resp.text
# US-003 (PRD 5.10): Blocatele apar ca pill-uri (nu ca lista cu ID-uri)
assert "Necesita atentie" in html, (
f"Fragmentul nu contine sectiunea 'Necesita atentie'. HTML: {html[:800]}"
)
# Pill-urile au etichetele scurte per categorie (nu etichetele lungi din eticheta_stare)
assert "Lipsa cod" in html, (
"Fragmentul nu arata pill-ul pentru needs_mapping"
)
assert "Date incomplete" in html, (
"Fragmentul nu arata pill-ul pentru needs_data"
)
assert "Eroare" in html, (
"Fragmentul nu arata pill-ul pentru error"
)
assert "Lipsa cod" in html, "Acasa nu arata pill-ul pentru needs_mapping"
assert "Date incomplete" in html, "Acasa nu arata pill-ul pentru needs_data"
assert "Eroare" in html, "Acasa nu arata pill-ul pentru error"
# Pill-urile arata numarul total per categorie (2 needs_mapping, 1 needs_data, 1 error)
assert "2" in html, "Pill-ul needs_mapping trebuie sa arate numarul 2"
assert 'class="pill-cat"' in html, "Pill-urile trebuie sa fie elemente cu clasa pill-cat"
assert "<button" in html, "Pill-urile trebuie sa fie elemente <button>"
@@ -207,16 +197,15 @@ def _insert_submission_vehicul(status, account_id, vin, nr):
def test_categorie_blocata_linkeaza_la_trimiteri_filtrate(client):
"""US-003 (PRD 5.10): pill-ul error are hx-get cu ?status=error.
Deep-link-ul tab=acasa&status=error a fost eliminat (pill inlocuieste link-ul vechi)."""
"""Pill-ul error filtreaza tabelul prin filtreazaStare(this, 'error') in bara de filtre."""
acct_id, _ = _create_account_user("link@test.com", "parolasecreta10")
_login(client, "link@test.com", "parolasecreta10")
_insert_submission("error", acct_id)
html = client.get("/_fragments/status").text
# Pill-ul are hx-get cu status=error (filtrare directa submissions)
assert "/_fragments/submissions?status=error" in html
# Deep-link-ul tab=acasa&status=error nu mai exista — pill-uri inlocuiesc link-urile
html = client.get("/?tab=acasa").text
# Pill-ul scrie campul de filtru si re-trimite form-ul (nu mai navigheaza prin deep-link)
assert "filtreazaStare(this, 'error')" in html
assert 'data-status="error"' in html
assert "tab=acasa&status=error" not in html
@@ -228,12 +217,10 @@ def test_status_nu_arata_identificator_rand_blocat(client):
_insert_submission_vehicul("error", acct_id, "WVWZZZ1KZAW000123", "B123ABC")
html = client.get("/_fragments/status").text
# Bara de status arata pill cu count, nu lista cu VIN/nr per rand
# Bara de status arata doar contoare, nu lista cu VIN/nr per rand (fara PII nominal)
assert "B123ABC" not in html, "Nr inmatriculare nu trebuie sa mai apara in bara de status"
assert "WVWZZZ1KZAW000123" not in html, "VIN integral nu trebuie expus"
assert "0123" not in html, "VIN partial nu trebuie sa mai apara in bara de status"
# Pill-ul cu count 1 apare in locul listei
assert "status=error" in html, "Pill error trebuie sa aiba hx-get cu status=error"
def test_scoped_pe_cont(client):