feat(ux): import compact + preview format Trimiteri + navigatie + scoatere auto_send (5.11)

8 stories TDD (echipa Sonnet, lead orchestreaza). US-001 scoate hold-ul auto_send din mapare
(has_no_auto_send->False, simbol pastrat; cod rezolvat->queued). US-002 scoate bifa auto_send
din UI. US-003 preview pas 3 in format .tabel-trimiteri (STARI_PREVIEW + nota_umana_preview,
fara repr Python; view-model prez). US-004 filtre layout/stil ca referinta + buton Custom.
US-005 navigatie Trimiteri/Mapari sub contoare pe toate paginile. US-006 import <details> nativ
colapsabil. US-007 post-commit reveal (OOB _coada/_status + HX-Trigger). US-008 auto-refresh
dupa actiuni (nudge eliminat).

VERIFY context curat PASS (8/8). /code-review high: 3 buguri reparate (tab nav la self-refresh,
pill Custom valori stale, nota_umana_preview precedenta needs_mapping). 934 passed, 1 skipped.
Backend trimitere + schema NEATINSE.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-06-26 15:16:28 +00:00
parent 412102b9b1
commit 283299ff20
34 changed files with 3079 additions and 389 deletions

View File

@@ -29,12 +29,14 @@ from ..payload_view import prezentare_din_payload
from ..web.csrf import get_csrf_token, verify_csrf
from .labels import (
ETICHETA_ULTIMA_AUTENTIFICARE_RAR,
STARI_PREVIEW,
eticheta_rar,
eticheta_scurta,
eticheta_stare,
eticheta_worker,
format_data_rar,
motiv_uman,
nota_umana_preview,
parse_erori,
)
from ..web.session import require_login
@@ -593,6 +595,40 @@ def _pills_categorii(counts: dict[str, int]) -> list[dict]:
]
def _build_status_ctx(request: Request, conn, account_id: int, *, oob: bool = False, tab_activ: str = "acasa") -> dict:
"""Construieste dictionarul de context pentru _status.html.
Accepta o conexiune deja deschisa (nu deschide alta). Folosit de fragment_status
si de web_confirma_import (OOB swap dupa commit).
"""
counts = _status_counts(conn, account_id)
hb = read_heartbeat(conn)
worker_alive = _worker_alive(hb)
rar_state = _rar_state(hb, worker_alive)
worker_lbl = eticheta_worker(worker_alive)
rar_ok = rar_state == "ok"
rar_lbl = eticheta_rar("ok" if rar_ok else rar_state)
blocate_total = sum(counts.get(s, 0) for s in _BLOCKED)
return {
"request": request,
"worker_lbl": worker_lbl,
"rar_lbl": rar_lbl,
"worker_ok": worker_alive,
"rar_ok": rar_ok,
"eticheta_ultima_auth": ETICHETA_ULTIMA_AUTENTIFICARE_RAR,
"last_login": format_data_rar(hb["last_rar_login_ok"] if hb else None),
"counts_queued": counts.get("queued", 0),
"counts_sent": counts.get("sent", 0),
"blocate_total": blocate_total,
"blocate_defalcat": _blocate_defalcat(counts),
"pills_categorii": _pills_categorii(counts),
"account_active": _account_active(conn, account_id),
"tab_activ": tab_activ,
"mapari_badge": counts.get("needs_mapping", 0),
"oob": oob,
}
@router.get("/_fragments/status", response_class=HTMLResponse)
def fragment_status(request: Request) -> HTMLResponse:
"""Bara de status persistenta cu etichete umane.
@@ -604,34 +640,9 @@ def fragment_status(request: Request) -> HTMLResponse:
account_id = require_login(request)
conn = get_connection()
try:
counts = _status_counts(conn, account_id)
hb = read_heartbeat(conn)
worker_alive = _worker_alive(hb)
rar_state = _rar_state(hb, worker_alive)
# Etichete umane pre-calculate (nu logica in template)
worker_lbl = eticheta_worker(worker_alive)
# eticheta_rar accepta "ok" sau orice alt string -> indisponibil/necunoscut
rar_ok = rar_state == "ok"
rar_lbl = eticheta_rar("ok" if rar_ok else rar_state)
blocate_total = sum(counts.get(s, 0) for s in _BLOCKED)
return templates.TemplateResponse("_status.html", {
"request": request,
"worker_lbl": worker_lbl,
"rar_lbl": rar_lbl,
# Stari binare pentru bife accesibile: glifa + culoare
"worker_ok": worker_alive,
"rar_ok": rar_ok,
"eticheta_ultima_auth": ETICHETA_ULTIMA_AUTENTIFICARE_RAR,
"last_login": format_data_rar(hb["last_rar_login_ok"] if hb else None),
"counts_queued": counts.get("queued", 0),
"counts_sent": counts.get("sent", 0),
"blocate_total": blocate_total,
"blocate_defalcat": _blocate_defalcat(counts),
"pills_categorii": _pills_categorii(counts),
"account_active": _account_active(conn, account_id),
})
tab_activ = request.query_params.get("tab", "acasa")
ctx = _build_status_ctx(request, conn, account_id, tab_activ=tab_activ)
return templates.TemplateResponse("_status.html", ctx)
finally:
conn.close()
@@ -1163,21 +1174,7 @@ async def post_corectie_trimitere(request: Request, submission_id: int) -> HTMLR
message="Lipseste inca un cod RAR — alege-l mai jos sau in tab-ul Mapari."),
)
if has_no_auto_send(resolved, mapping_meta):
conn.execute(
"UPDATE submissions SET status='needs_mapping', payload_json=?, rar_error=?, "
"updated_at=datetime('now') WHERE id=?",
(payload_json,
json.dumps({"auto_send": "cod mapat cu auto_send=0; review manual inainte de trimitere"},
ensure_ascii=False),
row["id"]),
)
row2 = _fetch_submission_scoped(conn, account_id, submission_id)
return templates.TemplateResponse(
"_trimitere_detaliu.html",
_detaliu_ctx(request, row2, error=True,
message="Cod cu auto-send oprit — confirma manual din tab-ul Mapari."),
)
# US-001 (PRD 5.11): ramura auto_send eliminata din corectie.
errors = validate_prezentare(content)
if errors:
@@ -2025,6 +2022,29 @@ def _web_compute_preview(
except Exception:
conn.execute("ROLLBACK")
# Enrichment UI: adauga campuri pre-computate necesare template-ului.
# Toate consumatorii (preview complet, rand single via _preview_one_row)
# obtin automat campurile adaugate aici.
for row in preview_rows:
# view-model prez (vehicul/operatie/cod RAR) — prezentare_din_payload
# accepta dict direct (nu e nevoie de serializare/deserializare JSON).
row["prez"] = prezentare_din_payload(row["resolved"])
# Eticheta umana + clasa CSS pentru pill — din STARI_PREVIEW, nu STARI_SUBMISSION
# (eticheta_stare ridica KeyError pe ok/already_sent/duplicate_in_file).
_etq, _css = STARI_PREVIEW.get(
row["resolved_status"],
(row["resolved_status"], f"s-{row['resolved_status']}"),
)
row["stare_eticheta"] = _etq
row["stare_css"] = _css
# Nota umana formatata — errors e lista Python, NU JSON string;
# nota_umana_preview o trateaza corect (fara repr Python brut in Note).
row["nota_umana"] = nota_umana_preview(
row["resolved_status"],
row.get("errors") or [],
row.get("flags") or [],
)
nomenclator = load_nomenclator(conn)
return {
"rows": preview_rows,
@@ -2750,18 +2770,33 @@ async def web_confirma_import(
(n_enqueued, import_id),
)
# Succes → bara de upload slim cu mesaj de confirmare. are_trimiteri=True:
# contul tocmai a pus randuri in coada -> bara ramane slim si dezvaluie
# sectiunea "Trimiterile tale" de pe Acasa.
# Succes → bara de upload slim cu mesaj de confirmare + OOB swap al
# #trimiteri-section (injecteaza _coada.html cu lista proaspata) +
# header HX-Trigger: trimiteriChanged (declanseza reincarcarea automata).
toctou_msg = f" ({len(toctou)} coliziuni TOCTOU excluse)" if toctou else ""
return templates.TemplateResponse("_upload.html", _ctx(
request,
are_trimiteri=True,
message=(
f"S-au pus in coada {n_enqueued} prezentari{toctou_msg}. "
f"Procesarea incepe in cateva secunde — vezi mai jos, in Trimiterile tale."
),
))
succes_msg = (
f"S-au pus in coada {n_enqueued} prezentari{toctou_msg}. "
f"Procesarea incepe in cateva secunde — vezi mai jos, in Trimiterile tale."
)
# Calculeaza contextele (necesita conn deschis) inainte de finally.
acasa_ctx = _get_acasa_context(request, conn, account_id)
acasa_ctx["status_filtru"] = ""
acasa_ctx["oob"] = True # adauga hx-swap-oob="outerHTML" la <section>
status_ctx = _build_status_ctx(request, conn, account_id, oob=True)
# Randeaza imediat (conn inca deschis — query-urile s-au facut mai sus).
upload_html = templates.get_template("_upload.html").render(
_ctx(request, are_trimiteri=True, message=succes_msg)
)
coada_html = templates.get_template("_coada.html").render(acasa_ctx)
status_html = templates.get_template("_status.html").render(status_ctx)
return HTMLResponse(
content=upload_html + "\n" + coada_html + "\n" + status_html,
headers={"HX-Trigger": "trimiteriChanged"},
)
finally:
conn.close()