feat(header): badge mediu RAR compact langa chip-ul RAR

Muta indicatorul de mediu (Testare/PRODUCTIE) din statusbar (randare
periodica, rand intreg) intr-un badge compact din header, langa chipul
RAR scurtat la "RAR". Click pe badge comuta mediul cand exista 2
disponibile; pe mobil (<=560px) eticheta scurta PROD/TEST ramane mereu
vizibila (semnal de risc L.142). Toggle-ul din header si schimbarea
mediului implicit din Setari actualizeaza badge-ul via OOB swap.

Co-Authored-By: Claude Agent <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-07-06 21:15:12 +00:00
parent d7adeaf382
commit a46e364594
5 changed files with 185 additions and 91 deletions

View File

@@ -400,6 +400,9 @@ def _get_acasa_context(request: Request, conn, account_id: int) -> dict:
"csrf_token": get_csrf_token(request),
# Banner ne-blocant: contul nu are identitate completa (companie+email+CUI)
"cont_incomplet": cont_incomplet,
# Mediul RAR pentru caseta de upload (_upload.html): fara el, template-ul
# ar vedea medii=[] si ar afisa fals "Niciun mediu RAR activ".
**_import_env_ctx(conn, acct),
}
@@ -420,12 +423,15 @@ def _render_panel_acasa(request: Request, conn=None, account_id: int = 1, status
return templates.get_template("_acasa.html").render(ctx)
def _render_panel_import(request: Request) -> str:
def _render_panel_import(request: Request, conn=None, account_id: int = 1) -> str:
"""Randeaza panoul Import ca string HTML (include _upload.html)."""
return templates.get_template("_upload.html").render({
ctx = {
"request": request,
"csrf_token": get_csrf_token(request),
})
}
if conn is not None:
ctx.update(_import_env_ctx(conn, account_id))
return templates.get_template("_upload.html").render(ctx)
def _render_panel_coada(request: Request, conn=None, account_id: int = 1) -> str:
@@ -448,12 +454,18 @@ def _render_panel_mapari(request: Request, conn, account_id: int) -> str:
})
def _render_panel_cont(request: Request, conn, account_id: int) -> str:
"""Randeaza panoul Cont ca string HTML."""
def _render_panel_cont(request: Request, conn, account_id: int, activ_subtab: str = "cont") -> str:
"""Randeaza panoul Cont ca string HTML.
`activ_subtab` permite deep-link pe un sub-tab din Setari (?tab=cont&subtab=rar):
'cont' | 'rar' | 'api'; valoare invalida -> 'cont'.
"""
from ..mapping import account_or_default
acct = account_or_default(account_id)
account_meta = _fetch_account_meta(conn, acct)
env_ctx = _fetch_cont_env_state(conn, acct)
if activ_subtab not in ("cont", "rar", "api"):
activ_subtab = "cont"
cont_ctx = {
"request": request,
"csrf_token": get_csrf_token(request),
@@ -464,7 +476,7 @@ def _render_panel_cont(request: Request, conn, account_id: int) -> str:
"account_meta": account_meta,
"date_firma_mesaj": None,
"date_firma_eroare": None,
"activ_subtab": "cont",
"activ_subtab": activ_subtab,
"parola_mesaj": None,
"parola_eroare": None,
"creds_test_mesaj": None,
@@ -604,20 +616,20 @@ def _render_panel_jurnal(request: Request, conn, account_id: int) -> str:
return templates.get_template("_jurnal.html").render(_jurnal_context(request, conn, account_id))
def _render_panel_for_tab(request: Request, conn, account_id: int, tab: str, status: str | None = None) -> str:
def _render_panel_for_tab(request: Request, conn, account_id: int, tab: str, status: str | None = None, subtab: str | None = None) -> str:
"""Randeaza panoul corespunzator unui tab ca string HTML."""
if tab == "acasa":
return _render_panel_acasa(request, conn, account_id, status=status)
if tab == "jurnal":
return _render_panel_jurnal(request, conn, account_id)
if tab == "import":
return _render_panel_import(request)
return _render_panel_import(request, conn, account_id)
if tab == "coada":
return _render_panel_coada(request, conn, account_id)
if tab == "mapari":
return _render_panel_mapari(request, conn, account_id)
if tab == "cont":
return _render_panel_cont(request, conn, account_id)
return _render_panel_cont(request, conn, account_id, activ_subtab=subtab or "cont")
if tab == "nomenclator":
return _render_panel_nomenclator(request, conn)
if tab == "integrare":
@@ -722,7 +734,7 @@ def _plan_ctx(conn, account_id: int, now: datetime | None = None) -> dict:
}
def _layout_header_ctx(conn, account_id: int) -> dict:
def _layout_header_ctx(request: Request, conn, account_id: int) -> dict:
"""Context suplimentar pentru antetul branduit.
Citeste account_name, tier si starea de sanatate RAR pentru a popula:
@@ -730,6 +742,7 @@ def _layout_header_ctx(conn, account_id: int) -> dict:
- tier_label: eticheta planului (Gratuit/Standard/Pro/Premium)
- sanatate_ok: True daca worker viu si RAR ok (dot verde in antet)
- last_login: data/ora ultimei autentificari RAR (format romanesc)
- medii_disponibile + env_default: badge-ul de mediu RAR din header
- plan_linie + plan_warn + ...: context plan pentru burger
"""
row = conn.execute(
@@ -753,6 +766,10 @@ def _layout_header_ctx(conn, account_id: int) -> dict:
# Starea comutatorului "Trimite automat la RAR" pentru toggle-ul din clusterul
# de header — randat in afara swap-ului periodic al barei de status.
"auto_send_enabled": get_auto_send(conn, account_id),
# Badge mediu RAR compact din header (langa chip-ul RAR).
"medii_disponibile": medii_disponibile_cont(conn, account_id),
"env_default": rar_env_efectiv_cont(conn, account_id) or "prod",
"csrf_token": get_csrf_token(request),
}
# Context plan pentru linia detaliata din meniul burger.
ctx.update(_plan_ctx(conn, account_id))
@@ -760,7 +777,7 @@ def _layout_header_ctx(conn, account_id: int) -> dict:
@router.get("/", response_class=HTMLResponse)
def dashboard(request: Request, tab: str = "acasa", status: str | None = None) -> HTMLResponse:
def dashboard(request: Request, tab: str = "acasa", status: str | None = None, subtab: str | None = None) -> HTMLResponse:
"""Dashboard principal cu tab-uri.
Parametrul ?tab= permite deep-link pe orice sectiune; panoul activ e randat
@@ -788,7 +805,7 @@ def dashboard(request: Request, tab: str = "acasa", status: str | None = None) -
active_tab = tab if tab in _TABS_VALIDE else "acasa"
conn = get_connection()
try:
panel_html = _render_panel_for_tab(request, conn, account_id, active_tab, status=status)
panel_html = _render_panel_for_tab(request, conn, account_id, active_tab, status=status, subtab=subtab)
# Badge contoare pe tab-uri: needs_mapping -> Mapari. Blocatele se reflecta in
# heading-ul sectiunii Trimiteri.
counts = _status_counts(conn, account_id)
@@ -807,7 +824,7 @@ def dashboard(request: Request, tab: str = "acasa", status: str | None = None) -
"csrf_token": get_csrf_token(request),
}
# Context antet (account_name, tier, sanatate RAR).
ctx.update(_layout_header_ctx(conn, account_id))
ctx.update(_layout_header_ctx(request, conn, account_id))
return templates.TemplateResponse("dashboard.html", ctx)
finally:
conn.close()
@@ -984,10 +1001,6 @@ def _build_status_ctx(request: Request, conn, account_id: int, *, oob: bool = Fa
else:
sanatate_text = "Declaratiile curg normal"
# Mediul RAR activ per cont pentru indicatorul din statusbar.
medii_disp = medii_disponibile_cont(conn, account_id)
env_default = rar_env_efectiv_cont(conn, account_id) or "prod"
# Contor "In asteptare (manual)" + banner coada tinuta imbatranita.
held_manual_count, held_aged_count = _held_counts(conn, account_id)
@@ -1012,9 +1025,6 @@ def _build_status_ctx(request: Request, conn, account_id: int, *, oob: bool = Fa
"tab_activ": tab_activ,
"mapari_badge": counts.get("needs_mapping", 0),
"oob": oob,
# Indicator mediu RAR + toggle conditionat
"env_default": env_default,
"medii_disponibile": medii_disp,
"auto_send_enabled": get_auto_send(conn, account_id),
"held_manual_count": held_manual_count,
"held_aged_count": held_aged_count,
@@ -2828,15 +2838,18 @@ def post_preview_regula_text(
pattern: str = Form(""),
csrf_token: str | None = Form(None),
) -> HTMLResponse:
"""Preview pre-salvare: cate operatii nemapate ar potrivi regula.
"""Preview pre-salvare: cate operatii nemapate ar potrivi regula + cod propus.
NU salveaza nimic (zero scriere DB). Normalizeaza pattern-ul cu
normalize_for_match, numara operatiile DISTINCTE nemapate ale contului
(needs_mapping, reuse pending_unmapped) al caror text (denumire sau
cod_op_service, normalizat) CONTINE pattern-ul + intoarce pana la 3 exemple.
Adauga propunerea automata de cod (GOLD partajat > SILVER > embeddings, via
enrich_suggestions) cu buton care preselecteaza codul in dropdown — fara salvare.
Pattern gol -> fragment gol (nu numara „tot"). Scoped pe contul sesiunii.
"""
from markupsafe import escape
from ..mapping import enrich_suggestions, load_nomenclator
account_id = require_login(request)
verify_csrf(request, csrf_token)
@@ -2846,9 +2859,32 @@ def post_preview_regula_text(
conn = get_connection()
try:
pending = pending_unmapped(conn, account_id)
# Propunere de cod pentru textul regulii (suggestion-only; embeddings sar
# peste model daca warmup-ul nu a terminat — degradare gratioasa).
enriched = enrich_suggestions(conn, (pattern or "").strip())
nume_coduri = {n["cod_prestatie"]: n["nume_prestatie"] for n in load_nomenclator(conn)}
finally:
conn.close()
_SURSE_UMANE = {"gold_partajat": "mapari validate", "silver": "etichetare LLM", "embedding": "similaritate"}
sugestie_html = ""
sp = enriched.get("sugestie_principala")
if sp and sp.get("cod_prestatie"):
cod = str(sp["cod_prestatie"])
nume = nume_coduri.get(cod, "")
sursa = _SURSE_UMANE.get(sp.get("sursa") or "", "")
cod_js = str(escape(cod)).replace("'", "")
sugestie_html = (
f'<div style="font-size:12px; margin-bottom:4px;">'
f'Propunere: <strong>{escape(cod)}</strong>'
f'{"" + str(escape(nume)) if nume else ""}'
f'{f" <span class=muted>({sursa})</span>" if sursa else ""}'
f' <button type="button" style="padding:1px 10px; font-size:11px; border-radius:99px;"'
f' onclick="var s=document.querySelector(\'select[name=cod_prestatie][form=rt-add]\');'
f'if(s){{s.value=\'{cod_js}\';s.dispatchEvent(new Event(\'change\',{{bubbles:true}}));}}">'
f'Foloseste</button></div>'
)
matches = []
for entry in pending:
text = normalize_for_match(entry.get("denumire") or entry.get("cod_op_service"))
@@ -2857,7 +2893,8 @@ def post_preview_regula_text(
if not matches:
return HTMLResponse(
content='<span class="muted" style="font-size:12px;">Nicio potrivire acum.</span>'
content=sugestie_html
+ '<span class="muted" style="font-size:12px;">Nicio potrivire acum.</span>'
)
exemple = ", ".join(
@@ -2868,7 +2905,8 @@ def post_preview_regula_text(
plural = "operatie nemapata" if n == 1 else "operatii nemapate"
return HTMLResponse(
content=(
f'<span class="muted" style="font-size:12px;">'
sugestie_html
+ f'<span class="muted" style="font-size:12px;">'
f'Potriveste {n} {plural}: {exemple}.</span>'
)
)
@@ -3941,7 +3979,7 @@ async def web_confirma_toate_review(
)
result = _web_compute_preview(conn, import_id, account_id)
if isinstance(result, str):
return templates.TemplateResponse("_upload.html", _ctx(request, error=result))
return templates.TemplateResponse("_upload.html", _ctx(request, error=result, **_import_env_ctx(conn, account_id)))
return templates.TemplateResponse("_preview_import.html", _ctx(
request, import_id=import_id, message=message, **result
))
@@ -3972,7 +4010,7 @@ async def web_mapare_operatie(
def _render(message: str | None = None, error: bool = False) -> HTMLResponse:
result = _web_compute_preview(conn, import_id, account_id)
if isinstance(result, str):
return templates.TemplateResponse("_upload.html", _ctx(request, error=result))
return templates.TemplateResponse("_upload.html", _ctx(request, error=result, **_import_env_ctx(conn, account_id)))
return templates.TemplateResponse("_preview_import.html", _ctx(
request, import_id=import_id, message=message, error=error, **result
))
@@ -4060,7 +4098,7 @@ async def web_mapare_operatii(
result = _web_compute_preview(conn, import_id, account_id)
if isinstance(result, str):
return templates.TemplateResponse("_upload.html", _ctx(request, error=result))
return templates.TemplateResponse("_upload.html", _ctx(request, error=result, **_import_env_ctx(conn, account_id)))
return templates.TemplateResponse("_preview_import.html", _ctx(
request, import_id=import_id, message=message, error=error, **result
))
@@ -4154,7 +4192,7 @@ async def web_confirma_import(
# Re-arata preview cu eroare
result = _web_compute_preview(conn, import_id, account_id)
if isinstance(result, str):
return templates.TemplateResponse("_upload.html", _ctx(request, error=result))
return templates.TemplateResponse("_upload.html", _ctx(request, error=result, **_import_env_ctx(conn, account_id)))
return templates.TemplateResponse("_preview_import.html", _ctx(
request,
import_id=import_id,
@@ -4215,7 +4253,7 @@ async def web_confirma_import(
)
_prev_result = _web_compute_preview(conn, import_id, account_id)
if isinstance(_prev_result, str):
return templates.TemplateResponse("_upload.html", _ctx(request, error=_err_msg))
return templates.TemplateResponse("_upload.html", _ctx(request, error=_err_msg, **_import_env_ctx(conn, account_id)))
return templates.TemplateResponse("_preview_import.html", _ctx(
request, import_id=import_id, message=_err_msg, error=True, **_prev_result
))
@@ -4228,7 +4266,7 @@ async def web_confirma_import(
f"Verifica preview-ul si retasteaza numarul corect."
)
if isinstance(result, str):
return templates.TemplateResponse("_upload.html", _ctx(request, error=msg))
return templates.TemplateResponse("_upload.html", _ctx(request, error=msg, **_import_env_ctx(conn, account_id)))
return templates.TemplateResponse("_preview_import.html", _ctx(
request, import_id=import_id, message=msg, error=True, **result
))
@@ -4236,7 +4274,7 @@ async def web_confirma_import(
if n_total_ok == 0:
result = _web_compute_preview(conn, import_id, account_id)
if isinstance(result, str):
return templates.TemplateResponse("_upload.html", _ctx(request, error=result))
return templates.TemplateResponse("_upload.html", _ctx(request, error=result, **_import_env_ctx(conn, account_id)))
return templates.TemplateResponse("_preview_import.html", _ctx(
request,
import_id=import_id,
@@ -5126,7 +5164,7 @@ async def cont_rar_medii(request: Request) -> HTMLResponse:
account_meta = _fetch_account_meta(conn, acct)
env_ctx = _fetch_cont_env_state(conn, acct)
return _render_cont(
resp = _render_cont(
request,
account_meta=account_meta,
creds_test_mesaj=creds_test_mesaj,
@@ -5138,6 +5176,12 @@ async def cont_rar_medii(request: Request) -> HTMLResponse:
activ_subtab="rar",
**env_ctx,
)
# Anexeaza badge-ul din header (OOB) — reflecta imediat activarea/dezactivarea
# unui mediu sau schimbarea mediului implicit, fara reload.
badge_html = templates.get_template("_header_env_badge.html").render(
_header_env_badge_ctx(request, conn, account_id, oob=True)
)
return HTMLResponse(content=bytes(resp.body).decode("utf-8") + "\n" + badge_html)
finally:
conn.close()
@@ -5200,12 +5244,24 @@ async def cont_schimba_parola(request: Request) -> HTMLResponse:
return _rerender(mesaj="Parola a fost schimbata.")
def _header_env_badge_ctx(request: Request, conn, account_id: int, *, oob: bool = False) -> dict:
"""Context pentru fragmentul badge-ului de mediu RAR din header."""
return {
"request": request,
"medii_disponibile": medii_disponibile_cont(conn, account_id),
"env_default": rar_env_efectiv_cont(conn, account_id) or "prod",
"csrf_token": get_csrf_token(request),
"oob": oob,
}
@router.post("/_fragments/status/toggle-env", response_class=HTMLResponse)
async def fragment_status_toggle_env(request: Request) -> HTMLResponse:
"""Comuta rar_env_default intre mediile disponibile ale contului.
Valideaza ca noul mediu e in lista mediilor disponibile inainte de UPDATE.
Intoarce statusbar-ul actualizat (acelasi format ca /_fragments/status).
Intoarce badge-ul din header actualizat (tinta hx-target) + status bar-ul
ca OOB swap, ca ambele sa ramana consecvente dupa comutare.
Ignorat silentios daca contul are un singur mediu sau zero medii disponibile.
"""
account_id = require_login(request)
@@ -5237,7 +5293,11 @@ async def fragment_status_toggle_env(request: Request) -> HTMLResponse:
)
conn.commit()
ctx = _build_status_ctx(request, conn, account_id, tab_activ="acasa")
return templates.TemplateResponse("_status.html", ctx)
badge_html = templates.get_template("_header_env_badge.html").render(
_header_env_badge_ctx(request, conn, account_id)
)
status_ctx = _build_status_ctx(request, conn, account_id, oob=True, tab_activ="acasa")
status_html = templates.get_template("_status.html").render(status_ctx)
return HTMLResponse(content=badge_html + "\n" + status_html)
finally:
conn.close()