feat(5.19): auto-send toggle per cont + tinere manuala randuri (held)

Comutator accounts.auto_send_enabled per cont: Auto OFF (default) tine randurile
la ingestie (submissions.held=1), worker-ul (claim_one AND held=0) le sare pana la
eliberare umana (per rand/bulk/auto-release OFF->ON). Snapshot held prin chokepoint
unic held_for_account pe toate caile de ingestie (API, import, reresolve, reactivare).

- schema/migrare: coloana held + index partial idx_submissions_held; auto_send_enabled
- API: echo onest held+motiv (US-010), ruta /prezentari/{id}/trimite-acum
- web: toggle header, modal confirmare tipata, buton Trimite per rand + Trimite toate,
  banner coada tinuta imbatranita (L.142), contor "In asteptare (manual)"
- worker: expire_held (US-008, inchide gaura retentie PII), metrics held gauges
- ops: tools/carantina_held + runbook rollback (R4)

Nota review (/code-review high): re-snapshot held lipseste pe caile repune/corectie
(requeue_submission, post_corectie, bulk-fix) — de aliniat separat cu create_prezentari.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-07-06 08:34:40 +00:00
parent 2ec3292382
commit 3b5cf7a7d9
30 changed files with 2344 additions and 32 deletions

View File

@@ -0,0 +1,54 @@
{#
_auto_send_toggle.html — comutatorul "Trimite automat la RAR" (PRD 5.19 US-004).
Plasat in clusterul de header (base.html, langa .rar-chip) — D1. NU face parte din
swap-ul periodic al #status-bar (every 15s) -> fara flicker / pierdere focus (D3).
Non-optimist (D2): la esec POST bifa revine + toast eroare (hx-on::response-error).
account_id vine din SESIUNE server-side (ruta /auto-send), niciodata din formular.
Raspunsul POST re-randeaza acest fragment (outerHTML pe #auto-send-toggle-wrap):
- confirmare_release=True -> injecteaza modalul de confirmare OOB in #modal-confirma-slot;
- curata_modal=True -> goleste slotul de modal (dupa activare reusita).
#}
<span id="auto-send-toggle-wrap" class="auto-send-wrap"
style="display:inline-flex; align-items:center; gap:6px;">
<input type="hidden" id="auto-send-csrf" name="csrf_token" value="{{ csrf_token | default('') }}">
<label class="auto-send-label"
title="Trimite automat la RAR. Debifat: prezentarile asteapta confirmarea ta."
style="display:inline-flex; align-items:center; gap:6px; cursor:pointer;
font-size:var(--fs-sm); white-space:nowrap; user-select:none;">
<input type="checkbox" name="enabled" value="1"
{% if auto_send_enabled|default(false) %}checked{% endif %}
hx-post="/auto-send"
hx-include="#auto-send-csrf"
hx-target="#auto-send-toggle-wrap"
hx-swap="outerHTML"
hx-on::response-error="this.checked = !this.checked; if(window.arataToast) window.arataToast('Nu am putut salva setarea Auto-send', 's-error');"
aria-label="Trimite automat la RAR"
style="width:16px; height:16px; cursor:pointer;">
<span class="auto-send-text">Trimite automat la RAR</span>
</label>
{% if confirmare_release|default(false) %}
{# OOB: modal de confirmare tipata pentru auto-release OFF->ON (garda F4/D4). #}
<span hx-swap-oob="innerHTML:#modal-confirma-slot">
{% set cf_id = 'modal-auto-release' %}
{% set cf_titlu = 'Activezi trimiterea automata?' %}
{% set cf_count = release_count %}
{% set cf_env = release_env %}
{% set cf_url = '/auto-send' %}
{% set cf_vals = '{"enabled":"1","confirma":"1"}' %}
{% set cf_target = '#auto-send-toggle-wrap' %}
{% set cf_swap = 'outerHTML' %}
{% set cf_confirm_label = 'Activez si trimit ' ~ release_count %}
{% set cf_deschis = true %}
{% include '_modal_confirma_trimitere.html' %}
</span>
{% endif %}
{% if curata_modal|default(false) %}
{# Curata slotul de modal dupa o comutare reusita (fara modal rezidual). #}
<span hx-swap-oob="innerHTML:#modal-confirma-slot"></span>
{% endif %}
</span>

View File

@@ -10,6 +10,38 @@
lista incepe direct sub filtre. Heading pastrat sr-only pentru a11y (section
aria-labelledby). Badge-ul de atentie + export CSV stau intr-un rand discret. #}
<h2 id="trimiteri-heading" class="sr-only">Trimiterile tale</h2>
{# US-005 (PRD 5.19): bulk "Trimite toate (N)" — N = randuri tinute (queued AND held=1).
Ascuns cand N=0. Confirmare tipata prin modal (count + mediu RAR), NU hx-confirm. #}
{% if held_manual_count|default(0) %}
<div style="display:flex; align-items:center; gap:8px; flex-wrap:wrap; margin:0 0 10px;
padding:8px 10px; border-left:3px solid var(--warn);
background:color-mix(in srgb, var(--warn) 10%, var(--card)); border-radius:6px;">
<span style="font-size:var(--fs-sm);">
<strong>{{ held_manual_count }}</strong>
{{ 'prezentare tinuta' if held_manual_count == 1 else 'prezentari tinute' }} manual.
</span>
<button type="button"
onclick="var m=document.getElementById('modal-trimite-toate'); if(m){m.hidden=false;}"
style="margin-left:auto; padding:4px 12px; font-size:13px; border-radius:6px;
border:1px solid var(--accent); background:var(--accent); color:#fff;
font-weight:600; cursor:pointer;">
Trimite toate ({{ held_manual_count }})
</button>
</div>
{% set cf_id = 'modal-trimite-toate' %}
{% set cf_titlu = 'Trimiti toate prezentarile tinute?' %}
{% set cf_count = held_manual_count %}
{% set cf_env = env_bulk | default('prod') %}
{% set cf_url = '/trimite-toate' %}
{% set cf_vals = '{}' %}
{% set cf_target = '#submissions-wrap' %}
{% set cf_swap = 'innerHTML' %}
{% set cf_confirm_label = 'Trimite toate (' ~ held_manual_count ~ ')' %}
{% set cf_deschis = false %}
{% include '_modal_confirma_trimitere.html' %}
{% endif %}
{% if blocate_total %}
<div style="display:flex; align-items:center; gap:6px; flex-wrap:wrap; margin:0 0 10px;">
<span class="tab-badge" title="{{ blocate_total }} necesita atentie"

View File

@@ -216,6 +216,52 @@
</div>
</div>
{# Card: De ce nu ajunge la RAR? (PRD 5.19 US-010 — onestitate canal API) #}
{# Un POST /v1/prezentari poate intoarce 200 dar randul sa NU plece la RAR. #}
<div class="card" style="margin-bottom:16px;">
<h3 style="margin:0 0 6px; font-size:15px;">De ce nu ajunge la RAR?</h3>
<p class="muted" style="font-size:12px; margin:0 0 12px;">
Un <code>POST /v1/prezentari</code> poate intoarce <strong>200</strong> cu
<code>status: "queued"</code>, dar randul sa NU plece automat la RAR. Cele trei motive:
</p>
<div class="banner warn" role="note" style="margin:0 0 12px; font-size:12.5px;">
<strong>Conturile noi pornesc cu Trimiterea automata OPRITA (Auto OFF).</strong>
Primul <code>POST</code> da 200/<code>queued</code>, dar randul e tinut
(<code>held=true</code>) si asteapta eliberare manuala. Verifica bifa
<em>„Trimite automat la RAR"</em> din dashboard.
</div>
<div class="tablewrap">
<table>
<thead><tr>
<th>Motiv</th>
<th>Ce inseamna</th>
<th>Cum deblochezi</th>
</tr></thead>
<tbody>
<tr>
<td><span class="pill">held</span> <span class="muted">(Auto OFF)</span></td>
<td>Contul are Trimiterea automata OPRITA &rarr; randul e tinut
(<code>held=true</code>), NU pleaca la RAR.</td>
<td>Elibereaza manual din dashboard (butonul <em>Trimite</em> / <em>Trimite toate</em>)
sau activeaza Trimiterea automata. Verifici cu
<code>GET /v1/prezentari/{id}</code> (<code>held=true</code>).</td>
</tr>
<tr>
<td><span class="pill">needs_mapping</span></td>
<td>Cod operatie fara mapare la un cod RAR din nomenclator.</td>
<td>Intra in editorul de mapare din dashboard; la salvarea maparii randul
se re-rezolva automat.</td>
</tr>
<tr>
<td><span class="pill">needs_data</span></td>
<td>Validare de continut esuata (VIN / data prestatie / odometru).</td>
<td>Corectezi datele si repui prezentarea.</td>
</tr>
</tbody>
</table>
</div>
</div>
{# Card: Export & referinta #}
<div class="card" style="margin-bottom:16px;">
<h3 style="margin:0 0 12px; font-size:15px;">Export &amp; referinta</h3>

View File

@@ -0,0 +1,67 @@
{#
_modal_confirma_trimitere.html — modal REAL de confirmare tipata (PRD 5.19 D4/F4/F5).
`hx-confirm` nativ (doar OK/Cancel) NU e suficient pentru o actiune ireversibila care
trimite N prezentari catre RAR. Acest modal arata numarul + mediul tinta (Testare /
PRODUCTIE) si — pentru PRODUCTIE — cere o confirmare tipata ("TRIMIT") inainte de a
activa butonul. Reutilizat din:
- _auto_send_toggle.html (auto-release OFF->ON), injectat OOB in #modal-confirma-slot;
- _coada.html (bulk "Trimite toate (N)"), randat inline ascuns si aratat la click.
Parametri (context):
cf_id, cf_titlu, cf_count, cf_env ('prod'/'test'), cf_url, cf_vals (json string),
cf_target, cf_swap, cf_confirm_label, cf_deschis (bool), csrf_token.
#}
{% set _prod = (cf_env == 'prod') %}
{% set _eb = eticheta_env(cf_env) %}
<div id="{{ cf_id }}" class="modal-overlay" role="dialog" aria-modal="true"
aria-labelledby="{{ cf_id }}-titlu"{% if not cf_deschis %} hidden{% endif %}>
<div class="modal-backdrop"
onclick="this.closest('.modal-overlay').hidden=true"></div>
<div class="modal-dialog" role="document" style="max-width:440px;">
<button type="button" class="modal-close" aria-label="Anuleaza"
onclick="this.closest('.modal-overlay').hidden=true">&times;</button>
<h2 id="{{ cf_id }}-titlu" style="margin:0 0 10px; font-size:var(--fs-lg);">{{ cf_titlu }}</h2>
<p style="margin:0 0 8px; font-size:var(--fs-sm);">
Se trimit imediat <strong>{{ cf_count }}</strong>
{{ 'prezentare' if cf_count == 1 else 'prezentari' }} catre
<span class="{{ _eb[1] }}">{{ _eb[0] }}</span>.
</p>
<p style="margin:0 0 12px; font-size:var(--fs-sm); color:var(--warn); font-weight:600;">
Declararea la RAR este ireversibila (FINALIZATA nu se poate anula).
</p>
{% if _prod %}
{# Type-to-confirm DOAR pe PRODUCTIE: butonul ramane dezactivat pana se scrie TRIMIT. #}
<label for="{{ cf_id }}-typ" style="display:block; font-size:var(--fs-xs); color:var(--muted); margin-bottom:4px;">
Scrie <strong>TRIMIT</strong> pentru a confirma:
</label>
<input type="text" id="{{ cf_id }}-typ" autocomplete="off" autocapitalize="characters"
style="width:100%; margin-bottom:12px; font-size:14px; padding:5px 8px;
border:1px solid var(--line); border-radius:5px; background:var(--card2); color:var(--ink);"
oninput="document.getElementById('{{ cf_id }}-ok').disabled = (this.value.trim().toUpperCase() !== 'TRIMIT');">
{% endif %}
<div style="display:flex; gap:8px; justify-content:flex-end; flex-wrap:wrap;">
<button type="button"
onclick="this.closest('.modal-overlay').hidden=true"
style="background:var(--card); color:var(--muted); border:1px solid var(--line);
padding:6px 14px; border-radius:6px; cursor:pointer; font-size:13px;">
Anuleaza
</button>
<button type="button" id="{{ cf_id }}-ok"{% if _prod %} disabled{% endif %}
hx-post="{{ cf_url }}"
hx-vals='{{ cf_vals }}'
hx-target="{{ cf_target }}"
hx-swap="{{ cf_swap }}"
hx-include="#{{ cf_id }}-csrf"
onclick="this.closest('.modal-overlay').hidden=true"
style="background:{% if _prod %}var(--err){% else %}var(--accent){% endif %};
color:#fff; border:none; padding:6px 14px; border-radius:6px;
cursor:pointer; font-size:13px; font-weight:600;">
{{ cf_confirm_label }}
</button>
<input type="hidden" id="{{ cf_id }}-csrf" name="csrf_token" value="{{ csrf_token }}">
</div>
</div>
</div>

View File

@@ -14,10 +14,36 @@
</div>
{% endif %}
{# === US-007 (PRD 5.19): banner coada tinuta imbatranita — mitigare OBLIGATORIE pt. default OFF.
Cand exista randuri `queued AND held=1` mai vechi de held_aged_days, semnalul de conformitate
L.142 (declarare obligatorie). Derivat (zero stare noua), varsta pe created_at.
Prioritate bannere (D7 — un-singur-banner "notice", ca sa nu impinga contoarele sub fold pe
mobil): cont inactiv (sus) + strip RAR jos raman separate; intre notice-uri, held-aged >
trial (risc legal > notita trial) -> trial e suprimat cand held-aged e activ. #}
{% set _held_aged_activ = (held_aged_count|default(0) > 0) and account_active %}
{% if _held_aged_activ %}
<div id="banner-held-aged" role="alert"
style="margin-bottom:10px; padding:8px 12px; border-left:3px solid var(--warn);
background:color-mix(in srgb, var(--warn) 12%, var(--card)); border-radius:6px;
font-size:var(--fs-sm); display:flex; align-items:center; gap:8px; flex-wrap:wrap;">
<span><strong>{{ held_aged_count }}</strong>
{{ 'prezentare tinuta' if held_aged_count == 1 else 'prezentari tinute' }}
de peste {{ held_aged_days }} zile — declarare obligatorie (L.142).</span>
<button type="button"
hx-get="/_fragments/submissions?held=1"
hx-target="#submissions-wrap" hx-swap="innerHTML"
style="margin-left:auto; padding:2px 10px; font-size:var(--fs-xs); border-radius:99px;
border:1px solid var(--warn); background:transparent; color:var(--warn);
cursor:pointer; font-weight:600;">
Vezi tinute
</button>
</div>
{% endif %}
{# US-006 (5.17) — Banner one-time trial->Gratuit (T-DES-1): afisat la prima incarcare
dupa expirarea trial-ului. Discret, non-blocant; dismissibil via sessionStorage.
Nu acopera stripul de sanatate (apare inainte de health strip, la acelasi nivel). #}
{% if trial_expirat_recent|default(false) %}
{% if trial_expirat_recent|default(false) and not _held_aged_activ %}
<div id="banner-trial-expirat"
role="status"
style="margin-bottom:10px; padding:7px 12px;
@@ -83,10 +109,17 @@
<div class="contor-label">Azi</div>
</div>
{# In coada #}
{# In coada — contorul "In asteptare (manual)" se pliaza aici (US-006/D6): nu adaugam a 6-a
celula, ci o sub-linie amber cand exista randuri tinute (queued AND held=1). #}
<div class="contor-card" style="flex:1; min-width:80px;">
<div class="contor-cifra s-queued">{{ counts_queued }}</div>
<div class="contor-label">In coada</div>
{% if held_manual_count|default(0) %}
<div class="contor-sub" style="font-size:var(--fs-xs); color:var(--warn); font-weight:600; margin-top:2px;"
title="Prezentari tinute manual — asteapta eliberarea ta">
{{ held_manual_count }} manual
</div>
{% endif %}
</div>
{# De corectat (rosu daca >0, muted la 0; link catre lista) #}
@@ -115,7 +148,7 @@
</div>
<div class="compact-item">
<div class="compact-nr s-queued">{{ counts_queued }}</div>
<div class="compact-lbl">Coada</div>
<div class="compact-lbl">Coada{% if held_manual_count|default(0) %} <span style="color:var(--warn); font-weight:700;">·{{ held_manual_count }}m</span>{% endif %}</div>
</div>
<a class="compact-item" href="/" style="text-decoration:none; color:inherit;">
<div class="compact-nr {{ 's-error' if blocate_total else 'muted' }}">{{ blocate_total }}</div>

View File

@@ -136,13 +136,33 @@
{% endif %}
</div>
{# Zona dreapta: pill stare + badge mediu RAR (US-010 PRD 5.20) #}
{# Zona dreapta: pill stare + badge mediu RAR (US-010 PRD 5.20) + buton Trimite (US-005) #}
<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>
{% if r.rar_env %}
{% set _eb = eticheta_env(r.rar_env) %}
<span class="{{ _eb[1] }}">{{ _eb[0] }}</span>
{% endif %}
{# US-005 (PRD 5.19 / D6): 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. #}
{% if r.tinut %}
<form hx-post="/trimitere/{{ r.id }}/trimite-acum"
hx-target="#submissions-wrap" hx-swap="innerHTML"
hx-confirm="Trimit aceasta prezentare la RAR? Declararea este ireversibila (FINALIZATA)."
onclick="event.stopPropagation();"
onkeydown="event.stopPropagation();"
style="margin:0;">
<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);
border:1px solid var(--accent); border-radius:99px; cursor:pointer;
background:var(--accent); color:#fff; font-weight:600;">
Trimite
</button>
</form>
{% endif %}
</div>
</li>
{% endfor %}

View File

@@ -868,6 +868,9 @@
<span class="rar-tx">RAR blocat</span>
</div>
{% endif %}
{# US-004 (PRD 5.19 / D1): comutatorul "Trimite automat la RAR" langa semnalul RAR.
In afara swap-ului periodic al #status-bar (D3). #}
{% include '_auto_send_toggle.html' %}
{% endif %}
{# US-011 (PRD 5.16): selector tema = pill cu icon FIX (acelasi SVG ca landing) +
eticheta temei curente. Eticheta ascunsa pe <=560px via CSS. JS actualizeaza
@@ -951,6 +954,10 @@
<div id="detaliu-modal-body"></div>
</div>
</div>
{# Slot pentru modalul de confirmare tipata (PRD 5.19 D4): auto-release OFF->ON injecteaza
aici modalul via OOB (#modal-confirma-slot). Bulk "Trimite toate" isi randeaza propriul
modal inline in _coada.html. #}
<div id="modal-confirma-slot"></div>
<script>
// Comutator tema ciclic (DRY E2 — PRD 5.15): config traieste intr-o singura structura
// sursa-de-adevar THEMES din care se DERIVA CYCLE/VALID/LABELS/NEXT.