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:
@@ -71,13 +71,40 @@ ETICHETE_SCURTE: dict[str, str] = {
|
||||
}
|
||||
|
||||
|
||||
def eticheta_scurta(status: str) -> str:
|
||||
# ---------------------------------------------------------------------------
|
||||
# Randuri tinute manual (held) — US-006, PRD 5.19
|
||||
#
|
||||
# `held` NU e o stare noua in masina de stari: un rand cu `status='queued' AND
|
||||
# held=1` este in coada, dar tinut pana la eliberarea umana. Il afisam distinct
|
||||
# ("In asteptare (manual)") cu culoare de AVERTIZARE (--warn / amber) — asteptare
|
||||
# benigna, NU eroare. Reutilizam clasa `s-needs_review` (definita in base.html ca
|
||||
# `color:var(--warn)`); NU folosim clasele needs_* rosii (s-needs_data /
|
||||
# s-needs_mapping = --err). CHECK-ul din schema.sql ramane neatins.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
ETICHETA_HELD: Eticheta = (
|
||||
"In asteptare (manual)",
|
||||
"Tinuta pentru confirmarea ta; nu pleaca spre RAR pana nu o eliberezi.",
|
||||
"s-needs_review",
|
||||
)
|
||||
|
||||
ETICHETA_HELD_SCURTA = "Manual"
|
||||
|
||||
|
||||
def eticheta_scurta(status: str, held: bool = False) -> str:
|
||||
"""
|
||||
Returneaza eticheta compacta (pill) pentru o stare de submission.
|
||||
|
||||
Cand `status='queued'` si `held` este adevarat, randul e tinut manual si
|
||||
intoarce "Manual" (distinct de "In coada") — vezi PRD 5.19 D5. Pentru orice
|
||||
alta stare `held` este ignorat, iar apelul fara `held` ramane identic cu cel
|
||||
dinainte de US-006.
|
||||
|
||||
Arunca KeyError daca starea nu este mapata — intentionat, ca sa prinda
|
||||
stari noi adaugate in schema fara mapare corespunzatoare.
|
||||
"""
|
||||
if held and status == "queued":
|
||||
return ETICHETA_HELD_SCURTA
|
||||
try:
|
||||
return ETICHETE_SCURTE[status]
|
||||
except KeyError:
|
||||
@@ -87,13 +114,20 @@ def eticheta_scurta(status: str) -> str:
|
||||
)
|
||||
|
||||
|
||||
def eticheta_stare(status: str) -> Eticheta:
|
||||
def eticheta_stare(status: str, held: bool = False) -> Eticheta:
|
||||
"""
|
||||
Returneaza (text, subtext, css_class) pentru o stare de submission.
|
||||
|
||||
Cand `status='queued'` si `held` este adevarat, randul e tinut manual si
|
||||
intoarce eticheta "In asteptare (manual)" cu clasa de avertizare (amber /
|
||||
--warn) — vezi PRD 5.19 US-006/D5. Pentru orice alta stare `held` este
|
||||
ignorat, iar apelul fara `held` ramane identic cu cel dinainte de US-006.
|
||||
|
||||
Arunca KeyError daca starea nu este mapata — intentionat, ca sa prinda
|
||||
stari noi adaugate in schema fara mapare corespunzatoare.
|
||||
"""
|
||||
if held and status == "queued":
|
||||
return ETICHETA_HELD
|
||||
try:
|
||||
return STARI_SUBMISSION[status]
|
||||
except KeyError:
|
||||
|
||||
@@ -53,6 +53,7 @@ from ..api.v1.import_router import (
|
||||
apply_row_override,
|
||||
EDIT_FIELDS,
|
||||
)
|
||||
from ..accounts import get_auto_send, set_auto_send
|
||||
from ..config import get_settings
|
||||
from ..crypto import decrypt_creds, encrypt_creds
|
||||
from ..db import get_connection, read_app_events, read_heartbeat
|
||||
@@ -245,6 +246,29 @@ def _status_counts(conn, account_id: int) -> dict[str, int]:
|
||||
return counts
|
||||
|
||||
|
||||
def _held_counts(conn, account_id: int) -> tuple[int, int]:
|
||||
"""Contoare randuri tinute manual (PRD 5.19 US-006/US-007) pentru contul din sesiune.
|
||||
|
||||
Intoarce (manual_count, aged_count):
|
||||
- manual_count = randuri `queued AND held=1` (contorul "In asteptare (manual)").
|
||||
- aged_count = subset mai vechi de `held_warn_days` (banner de conformitate L.142).
|
||||
Derivat, fara stare noua; varsta pe `created_at` (US-007 AC).
|
||||
"""
|
||||
days = int(get_settings().held_warn_days)
|
||||
row = conn.execute(
|
||||
"SELECT "
|
||||
" COUNT(*) AS manual, "
|
||||
" COUNT(CASE WHEN created_at < datetime('now', ?) THEN 1 END) AS aged "
|
||||
"FROM submissions "
|
||||
"WHERE status='queued' AND held=1 "
|
||||
" AND (account_id = ? OR (? = 1 AND account_id IS NULL))",
|
||||
(f"-{days} days", account_id, account_id),
|
||||
).fetchone()
|
||||
if not row:
|
||||
return (0, 0)
|
||||
return (int(row["manual"] or 0), int(row["aged"] or 0))
|
||||
|
||||
|
||||
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
|
||||
@@ -340,9 +364,17 @@ def _get_acasa_context(request: Request, conn, account_id: int) -> dict:
|
||||
counts = _status_counts(conn, account_id)
|
||||
blocate_total = sum(counts.get(s, 0) for s in _BLOCKED)
|
||||
|
||||
# US-005 (5.19): buton bulk "Trimite toate (N)" — N = randuri tinute ale contului.
|
||||
# env-ul tinta al eliberarii (badge in modalul de confirmare tipata).
|
||||
held_manual_count, _held_aged = _held_counts(conn, account_id)
|
||||
env_bulk = rar_env_efectiv_cont(conn, account_id) or "prod"
|
||||
|
||||
return {
|
||||
"request": request,
|
||||
"are_creds": are_creds,
|
||||
# US-005: coada tinuta manual + mediul RAR tinta pentru butonul "Trimite toate".
|
||||
"held_manual_count": held_manual_count,
|
||||
"env_bulk": env_bulk,
|
||||
"are_trimiteri": are_trimiteri,
|
||||
"are_cheie_folosita": are_cheie_folosita,
|
||||
"blocate_total": blocate_total,
|
||||
@@ -703,6 +735,9 @@ def _layout_header_ctx(conn, account_id: int) -> dict:
|
||||
"tier_label": tier_label,
|
||||
"sanatate_ok": sanatate_ok,
|
||||
"last_login": format_data_rar(hb["last_rar_login_ok"] if hb else None),
|
||||
# US-004 (5.19): starea comutatorului "Trimite automat la RAR" pentru toggle-ul
|
||||
# din clusterul de header (D1) — randat in afara swap-ului periodic al barei de status.
|
||||
"auto_send_enabled": get_auto_send(conn, account_id),
|
||||
}
|
||||
# US-006 (5.17): context plan pentru linia detaliata din meniul burger.
|
||||
ctx.update(_plan_ctx(conn, account_id))
|
||||
@@ -939,6 +974,9 @@ def _build_status_ctx(request: Request, conn, account_id: int, *, oob: bool = Fa
|
||||
medii_disp = medii_disponibile_cont(conn, account_id)
|
||||
env_default = rar_env_efectiv_cont(conn, account_id) or "prod"
|
||||
|
||||
# US-006/US-007 (5.19): contor "In asteptare (manual)" + banner coada tinuta imbatranita.
|
||||
held_manual_count, held_aged_count = _held_counts(conn, account_id)
|
||||
|
||||
status_ctx = {
|
||||
"request": request,
|
||||
"worker_lbl": worker_lbl,
|
||||
@@ -963,6 +1001,11 @@ def _build_status_ctx(request: Request, conn, account_id: int, *, oob: bool = Fa
|
||||
# US-011: indicator mediu RAR + toggle conditionat
|
||||
"env_default": env_default,
|
||||
"medii_disponibile": medii_disp,
|
||||
# US-006/US-007 (5.19): contor manual + banner coada tinuta imbatranita.
|
||||
"auto_send_enabled": get_auto_send(conn, account_id),
|
||||
"held_manual_count": held_manual_count,
|
||||
"held_aged_count": held_aged_count,
|
||||
"held_aged_days": int(get_settings().held_warn_days),
|
||||
"csrf_token": get_csrf_token(request),
|
||||
}
|
||||
# US-006 (5.17): context plan pentru linia de consum/trial in _status.html.
|
||||
@@ -1043,13 +1086,20 @@ def _eticheta_problema(status: str, motiv: str) -> str:
|
||||
|
||||
def _submission_row_view(r) -> dict:
|
||||
"""Imbogateste un rand de submission cu campuri afisabile umane."""
|
||||
eticheta = eticheta_stare(r["status"])
|
||||
# US-006 (PRD 5.19 / D5): camp derivat "tinut" calculat AICI (view-model), nu in
|
||||
# template. `held` e boolean pur de coada; eticheta/pill de avertizare (amber) se
|
||||
# obtin din labels.py cu held=True doar cand randul e `queued AND held=1`.
|
||||
held = bool(r["held"]) if ("held" in r.keys() and r["held"] is not None) else False
|
||||
tinut = held and r["status"] == "queued"
|
||||
eticheta = eticheta_stare(r["status"], held=tinut)
|
||||
motiv = motiv_uman(r["status"], r["rar_error"])
|
||||
return {
|
||||
"id": r["id"],
|
||||
"status": r["status"],
|
||||
# US-006: randul e tinut manual (buton "Trimite" apare DOAR pe el).
|
||||
"tinut": tinut,
|
||||
# pill = eticheta scurta; textul lung ramane ca tooltip (title=).
|
||||
"stare_scurt": eticheta_scurta(r["status"]),
|
||||
"stare_scurt": eticheta_scurta(r["status"], held=tinut),
|
||||
"stare_text": eticheta[0],
|
||||
"stare_css": eticheta[2],
|
||||
"prez": prezentare_din_payload(r["payload_json"]),
|
||||
@@ -1076,6 +1126,7 @@ def fragment_submissions(
|
||||
vehicul: str | None = None,
|
||||
data_de: str | None = None,
|
||||
data_pana: str | None = None,
|
||||
held: int | None = None,
|
||||
page: int = 1,
|
||||
) -> HTMLResponse:
|
||||
"""Tabel Trimiteri, scoped pe cont, cu filtre optionale si paginare.
|
||||
@@ -1090,7 +1141,7 @@ def fragment_submissions(
|
||||
vehicul_q = (vehicul or "").strip().upper() or None
|
||||
data_de = (data_de or "").strip() or None
|
||||
data_pana = (data_pana or "").strip() or None
|
||||
filtru_activ = bool(status or vehicul_q or data_de or data_pana)
|
||||
filtru_activ = bool(status or vehicul_q or data_de or data_pana or held == 1)
|
||||
filtru_python = bool(vehicul_q or data_de or data_pana) # filtru care necesita Python
|
||||
|
||||
page = max(1, page) # pre-clamp >= 1
|
||||
@@ -1103,6 +1154,10 @@ def fragment_submissions(
|
||||
if status:
|
||||
where.append("status=?")
|
||||
params.append(status)
|
||||
# US-007 (PRD 5.19): deep-link banner "coada tinuta" -> lista filtrata pe tinute
|
||||
# (queued AND held=1). Filtru derivat, fara stare noua.
|
||||
if held == 1:
|
||||
where.append("held=1")
|
||||
where_sql = " AND ".join(where)
|
||||
|
||||
if filtru_python:
|
||||
@@ -1110,7 +1165,7 @@ def fragment_submissions(
|
||||
# FARA LIMIT — altfel paginile >8 ar disparea silentios.
|
||||
rows_db = conn.execute(
|
||||
"SELECT id, status, id_prezentare, rar_status_code, rar_error, retry_count, "
|
||||
f"updated_at, payload_json, rar_env FROM submissions WHERE {where_sql} ORDER BY id DESC",
|
||||
f"updated_at, payload_json, rar_env, held FROM submissions WHERE {where_sql} ORDER BY id DESC",
|
||||
params,
|
||||
).fetchall()
|
||||
|
||||
@@ -1151,7 +1206,7 @@ def fragment_submissions(
|
||||
|
||||
rows_db = conn.execute(
|
||||
"SELECT id, status, id_prezentare, rar_status_code, rar_error, retry_count, "
|
||||
f"updated_at, payload_json, rar_env FROM submissions WHERE {where_sql} ORDER BY id DESC "
|
||||
f"updated_at, payload_json, rar_env, held FROM submissions WHERE {where_sql} ORDER BY id DESC "
|
||||
"LIMIT ? OFFSET ?",
|
||||
params + [_PAGE_SIZE, offset],
|
||||
).fetchall()
|
||||
@@ -1204,7 +1259,7 @@ def _render_submissions(request: Request, conn, account_id: int,
|
||||
scope_sql, scope_params = account_scope_clause(account_id)
|
||||
rows = conn.execute(
|
||||
"SELECT id, status, id_prezentare, rar_status_code, rar_error, retry_count, "
|
||||
f"updated_at, payload_json FROM submissions WHERE {scope_sql} ORDER BY id DESC LIMIT 200",
|
||||
f"updated_at, payload_json, rar_env, held FROM submissions WHERE {scope_sql} ORDER BY id DESC LIMIT 200",
|
||||
scope_params,
|
||||
).fetchall()
|
||||
view = [_submission_row_view(r) for r in rows]
|
||||
@@ -1927,6 +1982,168 @@ async def post_sterge_bulk(request: Request) -> HTMLResponse:
|
||||
conn.close()
|
||||
|
||||
|
||||
# =========================================================================== #
|
||||
# PRD 5.19 — comutator "Trimite automat la RAR" (US-004) + trimitere manuala #
|
||||
# per rand / bulk (US-005). Toate scoped pe contul sesiunii (NICIODATA din #
|
||||
# formular), CSRF obligatoriu, eliberare atomica (un singur UPDATE). #
|
||||
# =========================================================================== #
|
||||
|
||||
|
||||
def _auto_send_toggle_ctx(request: Request, conn, account_id: int) -> dict:
|
||||
"""Context minimal pentru fragmentul toggle `_auto_send_toggle.html`.
|
||||
|
||||
Reflecta starea persistata `accounts.auto_send_enabled` a contului din sesiune.
|
||||
"""
|
||||
return {
|
||||
"request": request,
|
||||
"auto_send_enabled": get_auto_send(conn, account_id),
|
||||
"csrf_token": get_csrf_token(request),
|
||||
"confirmare_release": False,
|
||||
"curata_modal": False,
|
||||
}
|
||||
|
||||
|
||||
@router.post("/auto-send", response_class=HTMLResponse)
|
||||
async def post_auto_send(request: Request) -> HTMLResponse:
|
||||
"""Comuta comutatorul "Trimite automat la RAR" (PRD 5.19 US-004), persistat pe cont.
|
||||
|
||||
`account_id` din SESIUNE (NICIODATA din formular). Non-optimist: raspunde cu toggle-ul
|
||||
re-randat din starea reala din DB (OOB outerHTML pe #auto-send-toggle-wrap).
|
||||
|
||||
Garda de confirmare OFF->ON (F4/D4): daca exista N>0 randuri `queued AND held=1`, NU
|
||||
comita ON pana la o confirmare tipata (modal) — intoarce toggle-ul pe starea veche +
|
||||
modalul de confirmare (OOB in #modal-confirma-slot). Cu `confirma=1`: comita ON si
|
||||
elibereaza randurile tinute printr-un SINGUR UPDATE atomic, scoped strict pe cont.
|
||||
ON->OFF nu retrage randuri deja eliberate.
|
||||
"""
|
||||
account_id = require_login(request)
|
||||
form = await request.form()
|
||||
verify_csrf(request, str(form.get("csrf_token") or ""))
|
||||
enabled = str(form.get("enabled") or "") not in ("", "false", "0", "off")
|
||||
confirma = str(form.get("confirma") or "") not in ("", "false", "0", "off")
|
||||
conn = get_connection()
|
||||
try:
|
||||
eliberate = 0
|
||||
if enabled:
|
||||
manual_count, _aged = _held_counts(conn, account_id)
|
||||
if manual_count > 0 and not confirma:
|
||||
# Garda: NU comita ON; toggle ramane pe starea din DB (OFF) + modal OOB.
|
||||
env_default = rar_env_efectiv_cont(conn, account_id) or "prod"
|
||||
ctx = _auto_send_toggle_ctx(request, conn, account_id)
|
||||
ctx.update({
|
||||
"confirmare_release": True,
|
||||
"release_count": manual_count,
|
||||
"release_env": env_default,
|
||||
})
|
||||
return templates.TemplateResponse("_auto_send_toggle.html", ctx)
|
||||
# Comita ON + eliberare atomica (single UPDATE scoped, NU loop).
|
||||
set_auto_send(conn, account_id, True)
|
||||
scope_sql, scope_params = account_scope_clause(account_id)
|
||||
cur = conn.execute(
|
||||
"UPDATE submissions SET held=0, updated_at=datetime('now') "
|
||||
f"WHERE status='queued' AND held=1 AND {scope_sql}",
|
||||
scope_params,
|
||||
)
|
||||
eliberate = int(cur.rowcount or 0)
|
||||
conn.commit()
|
||||
else:
|
||||
set_auto_send(conn, account_id, False)
|
||||
conn.commit()
|
||||
# Audit (US-009): comutarea + eventuala eliberare in bloc.
|
||||
log_event(
|
||||
"auto_send_schimbat", account_id=account_or_default(account_id),
|
||||
mesaj=f"auto_send -> {1 if enabled else 0}",
|
||||
context={"enabled": 1 if enabled else 0, "eliberate": eliberate}, conn=conn,
|
||||
)
|
||||
if enabled and eliberate:
|
||||
log_event(
|
||||
"held_eliberat", account_id=account_or_default(account_id),
|
||||
mesaj=f"eliberate {eliberate} (auto)",
|
||||
context={"count": eliberate, "mod": "auto"}, conn=conn,
|
||||
)
|
||||
# OOB: toggle re-randat (starea noua) + curata slotul de modal + refresh status/lista.
|
||||
ctx = _auto_send_toggle_ctx(request, conn, account_id)
|
||||
ctx["curata_modal"] = True
|
||||
resp = templates.TemplateResponse("_auto_send_toggle.html", ctx)
|
||||
resp.headers["HX-Trigger"] = "trimiteriChanged"
|
||||
return resp
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@router.post("/trimitere/{submission_id}/trimite-acum", response_class=HTMLResponse)
|
||||
async def post_trimite_acum(request: Request, submission_id: int) -> HTMLResponse:
|
||||
"""Trimitere manuala per rand (PRD 5.19 US-005): elibereaza un rand tinut (held 1->0).
|
||||
|
||||
Scoped pe sesiune (404-before-leak pe id strain via `_fetch_submission_scoped`).
|
||||
UPDATE include `AND status='queued' AND held=1` -> no-op sigur daca randul a fost deja
|
||||
luat de worker (`sending`) sau eliberat intre afisaj si click (edge race). Worker-ul
|
||||
preia randul la urmatorul poll (trimitere asincrona).
|
||||
"""
|
||||
account_id = require_login(request)
|
||||
form = await request.form()
|
||||
verify_csrf(request, str(form.get("csrf_token") or ""))
|
||||
conn = get_connection()
|
||||
try:
|
||||
row = _fetch_submission_scoped(conn, account_id, submission_id)
|
||||
if not row:
|
||||
raise HTTPException(status_code=404, detail="trimitere inexistenta")
|
||||
scope_sql, scope_params = account_scope_clause(account_id)
|
||||
cur = conn.execute(
|
||||
"UPDATE submissions SET held=0, updated_at=datetime('now') "
|
||||
f"WHERE id=? AND status='queued' AND held=1 AND {scope_sql}",
|
||||
[submission_id] + scope_params,
|
||||
)
|
||||
conn.commit()
|
||||
eliberate = int(cur.rowcount or 0)
|
||||
if eliberate:
|
||||
log_event(
|
||||
"held_eliberat", account_id=account_or_default(account_id),
|
||||
mesaj="eliberat 1 (rand)",
|
||||
context={"count": eliberate, "mod": "rand", "submission_id": submission_id},
|
||||
conn=conn,
|
||||
)
|
||||
resp = _render_submissions(request, conn, account_id)
|
||||
resp.headers["HX-Trigger"] = "trimiteriChanged"
|
||||
return resp
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@router.post("/trimite-toate", response_class=HTMLResponse)
|
||||
async def post_trimite_toate(request: Request) -> HTMLResponse:
|
||||
"""Trimitere manuala in bloc (PRD 5.19 US-005): elibereaza TOATE randurile tinute ale
|
||||
contului din sesiune (queued AND held=1 -> held=0) printr-un SINGUR UPDATE atomic scoped.
|
||||
|
||||
NU poate elibera randurile altui cont (scope strict pe sesiune). Worker-ul preia
|
||||
randurile la urmatorul poll.
|
||||
"""
|
||||
account_id = require_login(request)
|
||||
form = await request.form()
|
||||
verify_csrf(request, str(form.get("csrf_token") or ""))
|
||||
conn = get_connection()
|
||||
try:
|
||||
scope_sql, scope_params = account_scope_clause(account_id)
|
||||
cur = conn.execute(
|
||||
"UPDATE submissions SET held=0, updated_at=datetime('now') "
|
||||
f"WHERE status='queued' AND held=1 AND {scope_sql}",
|
||||
scope_params,
|
||||
)
|
||||
conn.commit()
|
||||
eliberate = int(cur.rowcount or 0)
|
||||
if eliberate:
|
||||
log_event(
|
||||
"held_eliberat", account_id=account_or_default(account_id),
|
||||
mesaj=f"eliberate {eliberate} (bulk)",
|
||||
context={"count": eliberate, "mod": "bulk"}, conn=conn,
|
||||
)
|
||||
resp = _render_submissions(request, conn, account_id)
|
||||
resp.headers["HX-Trigger"] = "trimiteriChanged"
|
||||
return resp
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
# =========================================================================== #
|
||||
# US-010 (PRD 5.15): Bulk-fix — aplica un cod RAR la selectia de randuri #
|
||||
# blocate. Reuse form #bulk-trimiteri + validare cod din post_corectie. #
|
||||
|
||||
54
app/web/templates/_auto_send_toggle.html
Normal file
54
app/web/templates/_auto_send_toggle.html
Normal 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>
|
||||
@@ -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"
|
||||
|
||||
@@ -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 → 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 & referinta</h3>
|
||||
|
||||
67
app/web/templates/_modal_confirma_trimitere.html
Normal file
67
app/web/templates/_modal_confirma_trimitere.html
Normal 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">×</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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user