feat(5.9): US-005 - poll-guard modal/bife pe trigger periodic

- base.html: listener htmx:beforeRequest scopat la #submissions-wrap care
  anuleaza (preventDefault) DOAR poll-ul periodic (fara requestConfig.triggeringEvent)
  cat timp modalul de detaliu e deschis SAU exista checkbox de bulk bifat.
- F5/R6: trimiteriChanged si submit-ul de filtru au triggeringEvent -> trec mereu,
  deci pauza nu ramane lipita permanent daca randul bifat paraseste filtrul.
- Resume automat (anularea nu opreste timer-ul htmx) + resume explicit pe checkbox
  change via delegare pe body -> trimiteriChanged from:body (pastreaza filtrul).
- Vechea pauza pe „rand expandat" (5.8) era deja inlocuita de modalul global (US-003).
- 3 teste noi in tests/test_web_modal.py; suita 843 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 09:30:10 +00:00
parent 35e97faae5
commit 74ac16f456
4 changed files with 143 additions and 3 deletions

View File

@@ -646,5 +646,47 @@
}); });
})(); })();
</script> </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.
(function() {
function modalDeschis() {
var o = document.getElementById('modal-detaliu');
return !!(o && !o.hidden);
}
function existaBifa() {
return !!document.querySelector('#submissions-wrap input[name="submission_id"]:checked');
}
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');
}
});
})();
</script>
</body> </body>
</html> </html>

View File

@@ -251,12 +251,12 @@
], ],
"requiresBrowserCheck": true, "requiresBrowserCheck": true,
"requiresDesignReview": false, "requiresDesignReview": false,
"passes": false, "passes": true,
"failed": false, "failed": false,
"blocked": false, "blocked": false,
"retries": 0, "retries": 0,
"failureReason": "", "failureReason": "",
"notes": "" "notes": "Poll-guard adaugat in base.html: htmx:beforeRequest scopat la #submissions-wrap, anuleaza (preventDefault) DOAR trigger-ul periodic (fara requestConfig.triggeringEvent) cat timp modalul e deschis SAU exista bifa de bulk. trimiteriChanged si submit-ul de filtru au triggeringEvent -> trec mereu (F5: fara blocaj permanent). Anularea nu opreste timer-ul htmx (se reprogrameaza) -> resume automat; resume explicit pe checkbox change via delegare pe body. Vechea pauza pe rand expandat (5.8) era deja inlocuita de modal (US-003). 3 teste noi in test_web_modal.py; suita 843 passed."
} }
] ]
} }

View File

@@ -231,3 +231,44 @@ Status: PASS (loop). E2E browser deferat la VERIFY.
- login/signup mosteneau deja `width:100%` inline + `margin:40px auto`; lipsea doar plafonarea - login/signup mosteneau deja `width:100%` inline + `margin:40px auto`; lipsea doar plafonarea
latimii pe mobil — rezolvata centralizat prin clasa `.auth-card` (fara duplicare de stil inline). latimii pe mobil — rezolvata centralizat prin clasa `.auth-card` (fara duplicare de stil inline).
--- ---
## US-005 — Poll-ul nu mai inchide modalul si nu mai sterge bifele (R6)
### Implementare (app/web/templates/base.html, +1 bloc <script>):
- Poll-guard nou: listener `htmx:beforeRequest` pe body, scopat la `#submissions-wrap`
(`d.elt.id !== 'submissions-wrap'` -> iese). Anuleaza (`evt.preventDefault()`) DOAR
trigger-ul periodic — in htmx `load`/`every 15s` declanseaza request FARA
`requestConfig.triggeringEvent`; `trimiteriChanged` si submit-ul/filtrul AU
triggeringEvent -> `if (rc.triggeringEvent) return;` => TREC MEREU.
- Pauza activa cat timp `modalDeschis()` (#modal-detaliu nu e hidden) SAU `existaBifa()`
(`#submissions-wrap input[name="submission_id"]:checked`).
- Resume: anularea unui beforeRequest NU opreste timer-ul htmx (`ot(e,t,r)` reprogrameaza
poll-ul indiferent de firing) -> reia automat la urmatorul tic cand ambele conditii dispar.
Resume explicit/imediat pe checkbox `change` (delegare pe body, prinde bifele post-swap):
cand modal inchis + nicio bifa -> `htmx.trigger(document.body, 'trimiteriChanged')`
(pastreaza filtrul via hx-include si trece de guard).
- Vechea pauza pe „rand expandat" (5.8) era deja eliminata de US-003 (detaliul inline ->
modal global in afara #submissions-wrap); guard-ul o inlocuieste conceptual.
### F5 — evitarea blocajului permanent:
- Pauza e legata STRICT de trigger-ul periodic (lipsa triggeringEvent), nu de o stare sticky.
Daca randul bifat paraseste filtrul, `trimiteriChanged`/submit-ul de filtru au triggeringEvent
si nu sunt niciodata anulate -> lista se reincarca, pauza nu ramane lipita.
### Teste (tests/test_web_modal.py, +3):
- test_poll_pauzat_cat_modal_deschis — guard scopat la #submissions-wrap + modalDeschis + preventDefault.
- test_poll_pauzat_cat_exista_bifa — existaBifa (`...submission_id]:checked`) + resume delegat pe `change`.
- test_trimiteriChanged_inca_reincarca_cu_bifa (R6/F5) — `rc.triggeringEvent) return` => trimiteriChanged/filtru trec.
- Structural (modal in afara #submissions-wrap) ramane acoperit de test_modal_container_in_afara_submissions_wrap (US-003).
### Gates:
- Tests: PASS — suita completa 843 passed, 1 deselected (era 840; +3 US-005).
- Browser (E2E requiresBrowserCheck): DEFERAT la VERIFY (bifez 2 trimiteri + astept >15s -> bifele raman;
deschid modalul + astept >15s -> ramane deschis cu datele intacte).
### Learnings:
- htmx pune pe pauza un poll fara a opri timer-ul daca anulezi `htmx:beforeRequest`: requestul curent
e abandonat, dar `processPolling` se reprogrameaza singur -> pauza „leneasa" fara cleanup de timer.
- Distinctia sursa periodic vs user/HX-Trigger se face curat prin `requestConfig.triggeringEvent`
(null pe load/poll, setat pe evenimente user/custom) — fara flag-uri ad-hoc.
---

View File

@@ -159,3 +159,60 @@ def test_modal_hookuri_js_prezente(client):
assert "inchideModal" in js assert "inchideModal" in js
# API public pastrat (butoanele/rutele pot inchide modalul) # API public pastrat (butoanele/rutele pot inchide modalul)
assert "window.inchideDetaliu" in js 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.
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")
_login(client, "poll1@test.com")
js = 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"
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")
_login(client, "poll2@test.com")
js = 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
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")
_login(client, "poll3@test.com")
js = 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