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), "csrf_token": get_csrf_token(request),
# Banner ne-blocant: contul nu are identitate completa (companie+email+CUI) # Banner ne-blocant: contul nu are identitate completa (companie+email+CUI)
"cont_incomplet": cont_incomplet, "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) 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).""" """Randeaza panoul Import ca string HTML (include _upload.html)."""
return templates.get_template("_upload.html").render({ ctx = {
"request": request, "request": request,
"csrf_token": get_csrf_token(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: 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: def _render_panel_cont(request: Request, conn, account_id: int, activ_subtab: str = "cont") -> str:
"""Randeaza panoul Cont ca string HTML.""" """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 from ..mapping import account_or_default
acct = account_or_default(account_id) acct = account_or_default(account_id)
account_meta = _fetch_account_meta(conn, acct) account_meta = _fetch_account_meta(conn, acct)
env_ctx = _fetch_cont_env_state(conn, acct) env_ctx = _fetch_cont_env_state(conn, acct)
if activ_subtab not in ("cont", "rar", "api"):
activ_subtab = "cont"
cont_ctx = { cont_ctx = {
"request": request, "request": request,
"csrf_token": get_csrf_token(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, "account_meta": account_meta,
"date_firma_mesaj": None, "date_firma_mesaj": None,
"date_firma_eroare": None, "date_firma_eroare": None,
"activ_subtab": "cont", "activ_subtab": activ_subtab,
"parola_mesaj": None, "parola_mesaj": None,
"parola_eroare": None, "parola_eroare": None,
"creds_test_mesaj": 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)) 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.""" """Randeaza panoul corespunzator unui tab ca string HTML."""
if tab == "acasa": if tab == "acasa":
return _render_panel_acasa(request, conn, account_id, status=status) return _render_panel_acasa(request, conn, account_id, status=status)
if tab == "jurnal": if tab == "jurnal":
return _render_panel_jurnal(request, conn, account_id) return _render_panel_jurnal(request, conn, account_id)
if tab == "import": if tab == "import":
return _render_panel_import(request) return _render_panel_import(request, conn, account_id)
if tab == "coada": if tab == "coada":
return _render_panel_coada(request, conn, account_id) return _render_panel_coada(request, conn, account_id)
if tab == "mapari": if tab == "mapari":
return _render_panel_mapari(request, conn, account_id) return _render_panel_mapari(request, conn, account_id)
if tab == "cont": 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": if tab == "nomenclator":
return _render_panel_nomenclator(request, conn) return _render_panel_nomenclator(request, conn)
if tab == "integrare": 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. """Context suplimentar pentru antetul branduit.
Citeste account_name, tier si starea de sanatate RAR pentru a popula: 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) - tier_label: eticheta planului (Gratuit/Standard/Pro/Premium)
- sanatate_ok: True daca worker viu si RAR ok (dot verde in antet) - sanatate_ok: True daca worker viu si RAR ok (dot verde in antet)
- last_login: data/ora ultimei autentificari RAR (format romanesc) - 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 - plan_linie + plan_warn + ...: context plan pentru burger
""" """
row = conn.execute( 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 # Starea comutatorului "Trimite automat la RAR" pentru toggle-ul din clusterul
# de header — randat in afara swap-ului periodic al barei de status. # de header — randat in afara swap-ului periodic al barei de status.
"auto_send_enabled": get_auto_send(conn, account_id), "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. # Context plan pentru linia detaliata din meniul burger.
ctx.update(_plan_ctx(conn, account_id)) 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) @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. """Dashboard principal cu tab-uri.
Parametrul ?tab= permite deep-link pe orice sectiune; panoul activ e randat 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" active_tab = tab if tab in _TABS_VALIDE else "acasa"
conn = get_connection() conn = get_connection()
try: 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 # Badge contoare pe tab-uri: needs_mapping -> Mapari. Blocatele se reflecta in
# heading-ul sectiunii Trimiteri. # heading-ul sectiunii Trimiteri.
counts = _status_counts(conn, account_id) 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), "csrf_token": get_csrf_token(request),
} }
# Context antet (account_name, tier, sanatate RAR). # 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) return templates.TemplateResponse("dashboard.html", ctx)
finally: finally:
conn.close() conn.close()
@@ -984,10 +1001,6 @@ def _build_status_ctx(request: Request, conn, account_id: int, *, oob: bool = Fa
else: else:
sanatate_text = "Declaratiile curg normal" 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. # Contor "In asteptare (manual)" + banner coada tinuta imbatranita.
held_manual_count, held_aged_count = _held_counts(conn, account_id) 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, "tab_activ": tab_activ,
"mapari_badge": counts.get("needs_mapping", 0), "mapari_badge": counts.get("needs_mapping", 0),
"oob": oob, "oob": oob,
# Indicator mediu RAR + toggle conditionat
"env_default": env_default,
"medii_disponibile": medii_disp,
"auto_send_enabled": get_auto_send(conn, account_id), "auto_send_enabled": get_auto_send(conn, account_id),
"held_manual_count": held_manual_count, "held_manual_count": held_manual_count,
"held_aged_count": held_aged_count, "held_aged_count": held_aged_count,
@@ -2828,15 +2838,18 @@ def post_preview_regula_text(
pattern: str = Form(""), pattern: str = Form(""),
csrf_token: str | None = Form(None), csrf_token: str | None = Form(None),
) -> HTMLResponse: ) -> 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 NU salveaza nimic (zero scriere DB). Normalizeaza pattern-ul cu
normalize_for_match, numara operatiile DISTINCTE nemapate ale contului normalize_for_match, numara operatiile DISTINCTE nemapate ale contului
(needs_mapping, reuse pending_unmapped) al caror text (denumire sau (needs_mapping, reuse pending_unmapped) al caror text (denumire sau
cod_op_service, normalizat) CONTINE pattern-ul + intoarce pana la 3 exemple. 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. Pattern gol -> fragment gol (nu numara „tot"). Scoped pe contul sesiunii.
""" """
from markupsafe import escape from markupsafe import escape
from ..mapping import enrich_suggestions, load_nomenclator
account_id = require_login(request) account_id = require_login(request)
verify_csrf(request, csrf_token) verify_csrf(request, csrf_token)
@@ -2846,9 +2859,32 @@ def post_preview_regula_text(
conn = get_connection() conn = get_connection()
try: try:
pending = pending_unmapped(conn, account_id) 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: finally:
conn.close() 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 = [] matches = []
for entry in pending: for entry in pending:
text = normalize_for_match(entry.get("denumire") or entry.get("cod_op_service")) 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: if not matches:
return HTMLResponse( 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( exemple = ", ".join(
@@ -2868,7 +2905,8 @@ def post_preview_regula_text(
plural = "operatie nemapata" if n == 1 else "operatii nemapate" plural = "operatie nemapata" if n == 1 else "operatii nemapate"
return HTMLResponse( return HTMLResponse(
content=( 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>' 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) result = _web_compute_preview(conn, import_id, account_id)
if isinstance(result, str): 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( return templates.TemplateResponse("_preview_import.html", _ctx(
request, import_id=import_id, message=message, **result 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: def _render(message: str | None = None, error: bool = False) -> HTMLResponse:
result = _web_compute_preview(conn, import_id, account_id) result = _web_compute_preview(conn, import_id, account_id)
if isinstance(result, str): 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( return templates.TemplateResponse("_preview_import.html", _ctx(
request, import_id=import_id, message=message, error=error, **result 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) result = _web_compute_preview(conn, import_id, account_id)
if isinstance(result, str): 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( return templates.TemplateResponse("_preview_import.html", _ctx(
request, import_id=import_id, message=message, error=error, **result request, import_id=import_id, message=message, error=error, **result
)) ))
@@ -4154,7 +4192,7 @@ async def web_confirma_import(
# Re-arata preview cu eroare # Re-arata preview cu eroare
result = _web_compute_preview(conn, import_id, account_id) result = _web_compute_preview(conn, import_id, account_id)
if isinstance(result, str): 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( return templates.TemplateResponse("_preview_import.html", _ctx(
request, request,
import_id=import_id, import_id=import_id,
@@ -4215,7 +4253,7 @@ async def web_confirma_import(
) )
_prev_result = _web_compute_preview(conn, import_id, account_id) _prev_result = _web_compute_preview(conn, import_id, account_id)
if isinstance(_prev_result, str): 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( return templates.TemplateResponse("_preview_import.html", _ctx(
request, import_id=import_id, message=_err_msg, error=True, **_prev_result 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." f"Verifica preview-ul si retasteaza numarul corect."
) )
if isinstance(result, str): 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( return templates.TemplateResponse("_preview_import.html", _ctx(
request, import_id=import_id, message=msg, error=True, **result request, import_id=import_id, message=msg, error=True, **result
)) ))
@@ -4236,7 +4274,7 @@ async def web_confirma_import(
if n_total_ok == 0: if n_total_ok == 0:
result = _web_compute_preview(conn, import_id, account_id) result = _web_compute_preview(conn, import_id, account_id)
if isinstance(result, str): 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( return templates.TemplateResponse("_preview_import.html", _ctx(
request, request,
import_id=import_id, import_id=import_id,
@@ -5126,7 +5164,7 @@ async def cont_rar_medii(request: Request) -> HTMLResponse:
account_meta = _fetch_account_meta(conn, acct) account_meta = _fetch_account_meta(conn, acct)
env_ctx = _fetch_cont_env_state(conn, acct) env_ctx = _fetch_cont_env_state(conn, acct)
return _render_cont( resp = _render_cont(
request, request,
account_meta=account_meta, account_meta=account_meta,
creds_test_mesaj=creds_test_mesaj, creds_test_mesaj=creds_test_mesaj,
@@ -5138,6 +5176,12 @@ async def cont_rar_medii(request: Request) -> HTMLResponse:
activ_subtab="rar", activ_subtab="rar",
**env_ctx, **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: finally:
conn.close() conn.close()
@@ -5200,12 +5244,24 @@ async def cont_schimba_parola(request: Request) -> HTMLResponse:
return _rerender(mesaj="Parola a fost schimbata.") 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) @router.post("/_fragments/status/toggle-env", response_class=HTMLResponse)
async def fragment_status_toggle_env(request: Request) -> HTMLResponse: async def fragment_status_toggle_env(request: Request) -> HTMLResponse:
"""Comuta rar_env_default intre mediile disponibile ale contului. """Comuta rar_env_default intre mediile disponibile ale contului.
Valideaza ca noul mediu e in lista mediilor disponibile inainte de UPDATE. 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. Ignorat silentios daca contul are un singur mediu sau zero medii disponibile.
""" """
account_id = require_login(request) account_id = require_login(request)
@@ -5237,7 +5293,11 @@ async def fragment_status_toggle_env(request: Request) -> HTMLResponse:
) )
conn.commit() conn.commit()
ctx = _build_status_ctx(request, conn, account_id, tab_activ="acasa") badge_html = templates.get_template("_header_env_badge.html").render(
return templates.TemplateResponse("_status.html", ctx) _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: finally:
conn.close() conn.close()

View File

@@ -0,0 +1,29 @@
{# Badge compact "Mediu RAR" din header, langa chip-ul RAR.
0 medii: wrapper gol (id-ul ramane in DOM pentru viitoare OOB swap).
1 mediu: eticheta statica. 2 medii: buton de comutare HTMX (ciclic).
Clasele .env-badge-prod/.env-badge-test sunt cele din listele de trimiteri (consecventa). #}
{% set _medii = medii_disponibile | default([]) %}
<div id="header-env-badge"{% if oob %} hx-swap-oob="outerHTML"{% endif %}>
{% if _medii | length >= 1 %}
{% set _env = env_default | default('prod') %}
{% set _eb = eticheta_env(_env) %}
{% set _scurt = 'PROD' if _env == 'prod' else 'TEST' %}
{% if _medii | length >= 2 %}
<button type="button" class="{{ _eb[1] }}"
style="cursor:pointer;"
hx-post="/_fragments/status/toggle-env"
hx-target="#header-env-badge"
hx-swap="outerHTML"
hx-vals='{"csrf_token": {{ csrf_token | tojson }}}'
title="Comuta la {{ 'Testare' if _env == 'prod' else 'PRODUCȚIE' }}">
<span class="env-tx-full">{{ _eb[0] }}</span>
<span class="env-tx-short">{{ _scurt }}</span>
</button>
{% else %}
<span class="{{ _eb[1] }}">
<span class="env-tx-full">{{ _eb[0] }}</span>
<span class="env-tx-short">{{ _scurt }}</span>
</span>
{% endif %}
{% endif %}
</div>

View File

@@ -156,43 +156,6 @@
</a> </a>
</div> </div>
{# === Indicator mediu RAR activ per cont.
La 0 medii: nu afisa (cont fara configuratie RAR).
La 1 mediu: eticheta statica (fara toggle).
La 2 medii: buton de comutare HTMX (toggle conditionat).
Culori prin eticheta_env/clase .env-badge-* (consistenta cu badge-urile din liste).
#}
{% set _medii = medii_disponibile | default([]) %}
{% set _env = env_default | default('prod') %}
{% set _eb = eticheta_env(_env) %}
{% if _medii | length >= 1 %}
<div class="env-indicator"
style="display:flex; align-items:center; gap:8px; margin-bottom:10px;
padding:6px 10px; border-radius:6px; font-size:var(--fs-sm);
background:color-mix(in srgb, {% if _env == 'prod' %}var(--err){% else %}var(--line){% endif %} 10%, var(--card));
border:1px solid color-mix(in srgb, {% if _env == 'prod' %}var(--err){% else %}var(--line){% endif %} 30%, transparent);">
<span style="font-weight:600;">Mediu RAR:</span>
<span class="{{ _eb[1] }}">{{ _eb[0] }}</span>
{% if _medii | length >= 2 %}
{# Toggle: apare DOAR cand sunt cel putin 2 medii disponibile #}
<form method="post" action="/_fragments/status/toggle-env"
hx-post="/_fragments/status/toggle-env"
hx-target="#status-bar"
hx-swap="outerHTML"
style="margin:0; padding:0; display:inline;">
<input type="hidden" name="csrf_token" value="{{ csrf_token | default('') }}">
<button type="submit"
style="padding:2px 10px; font-size:var(--fs-xs); border-radius:99px;
border:1px solid currentColor; background:transparent; cursor:pointer;
color:var(--muted); line-height:1.4;"
title="Comuta la {{ 'PRODUCȚIE' if _env == 'test' else 'Testare' }}">
Comuta
</button>
</form>
{% endif %}
</div>
{% endif %}
{# === Navigatie rapida: Trimiteri + Mapari cu badge needs_mapping === {# === Navigatie rapida: Trimiteri + Mapari cu badge needs_mapping ===
Pastrata exact ca inainte: tab_activ determina marcajul activ. Pastrata exact ca inainte: tab_activ determina marcajul activ.
#} #}

View File

@@ -150,6 +150,15 @@
background:color-mix(in srgb, var(--err) 80%, var(--card)); color:#fff; border:1px solid transparent; } background:color-mix(in srgb, var(--err) 80%, var(--card)); color:#fff; border:1px solid transparent; }
.env-badge-test { display:inline-block; padding:1px 7px; border-radius:99px; font-size:11px; .env-badge-test { display:inline-block; padding:1px 7px; border-radius:99px; font-size:11px;
background:transparent; color:var(--muted); border:1px solid var(--line); } background:transparent; color:var(--muted); border:1px solid var(--line); }
/* Badge mediu RAR din header: eticheta scurta PROD/TEST ramane mereu vizibila pe
mobil (semnal de risc L.142), eticheta lunga se ascunde sub 560px. Clasele
.env-badge-prod/.env-badge-test isi redefinesc deja border-ul complet, deci
suprascriu stilul global de buton fara reguli suplimentare. */
.env-tx-short { display:none; }
@media (max-width:560px) {
.env-tx-full { display:none; }
.env-tx-short { display:inline; }
}
.muted { color:var(--muted); } .muted { color:var(--muted); }
/* Heading/eticheta accesibila doar pentru cititoare de ecran (vizual ascunsa). */ /* Heading/eticheta accesibila doar pentru cititoare de ecran (vizual ascunsa). */
.sr-only { position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; .sr-only { position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden;
@@ -956,7 +965,7 @@
title="RAR online{% if last_login|default('') %} — Ultima autentificare: {{ last_login }}{% endif %}" title="RAR online{% if last_login|default('') %} — Ultima autentificare: {{ last_login }}{% endif %}"
aria-label="RAR online"> aria-label="RAR online">
<span class="rar-dot live" aria-hidden="true"></span> <span class="rar-dot live" aria-hidden="true"></span>
<span class="rar-tx">RAR online</span> <span class="rar-tx">RAR</span>
</div> </div>
{% else %} {% else %}
<div class="rar-chip rar-err" role="status" <div class="rar-chip rar-err" role="status"
@@ -966,6 +975,7 @@
<span class="rar-tx">RAR blocat</span> <span class="rar-tx">RAR blocat</span>
</div> </div>
{% endif %} {% endif %}
{% include '_header_env_badge.html' %}
{# Comutatorul "Trimite automat la RAR" langa semnalul RAR. {# Comutatorul "Trimite automat la RAR" langa semnalul RAR.
In afara swap-ului periodic al #status-bar. #} In afara swap-ului periodic al #status-bar. #}
{% include '_auto_send_toggle.html' %} {% include '_auto_send_toggle.html' %}

View File

@@ -1,13 +1,15 @@
"""Indicator mediu RAR in statusbar + toggle conditionat. """Badge mediu RAR compact in header + toggle conditionat.
Rute testate: Rute testate:
GET /_fragments/status -- indicator mediu (eticheta statica sau toggle) GET / -- badge mediu in header (eticheta statica sau toggle)
POST /_fragments/status/toggle-env -- comuta rar_env_default intre mediile disponibile POST /_fragments/status/toggle-env -- comuta rar_env_default intre mediile disponibile
POST /cont/rar-medii -- schimbare mediu implicit din Setari
Teste: Teste:
test_afiseaza_env_default -- un mediu disponibil -> eticheta statica, fara toggle test_afiseaza_env_default -- un mediu disponibil -> eticheta statica, fara toggle
test_toggle_doar_la_doua_medii -- doua medii -> toggle prezent; un mediu -> toggle absent test_toggle_doar_la_doua_medii -- doua medii -> toggle prezent; un mediu -> toggle absent
test_toggle_schimba_default -- POST toggle -> rar_env_default schimbat in DB, 200 test_toggle_schimba_default -- POST toggle -> rar_env_default schimbat in DB, badge+status OOB
test_rar_medii_actualizeaza_badge_oob -- schimbarea mediului implicit din Setari anexeaza badge OOB
""" """
from __future__ import annotations from __future__ import annotations
@@ -116,14 +118,14 @@ def _seteaza_mediu_disponibil(acct_id: int, env: str) -> None:
conn.close() conn.close()
def _get_csrf_din_status(client) -> str: def _get_csrf_din_pagina(client) -> str:
"""Obtine CSRF token din fragmentul /_fragments/status.""" """Obtine CSRF token din pagina completa `/` (header, randat o singura data)."""
resp = client.get("/_fragments/status") resp = client.get("/")
assert resp.status_code == 200, f"/_fragments/status a returnat {resp.status_code}" assert resp.status_code == 200, f"/ a returnat {resp.status_code}"
m = re.search(r'name="csrf_token"\s+value="([^"]+)"', resp.text) m = re.search(r'name="csrf_token"\s+value="([^"]+)"', resp.text)
if not m: if not m:
m = re.search(r'value="([^"]+)"\s+name="csrf_token"', resp.text) m = re.search(r'value="([^"]+)"\s+name="csrf_token"', resp.text)
assert m, f"csrf_token negasit in statusbar: {resp.text[:600]}" assert m, f"csrf_token negasit pe pagina: {resp.text[:600]}"
return m.group(1) return m.group(1)
@@ -133,7 +135,7 @@ def _get_csrf_din_status(client) -> str:
def test_afiseaza_env_default(client): def test_afiseaza_env_default(client):
"""Cont cu UN singur mediu disponibil (prod) -> statusbar contine eticheta pentru Productie, """Cont cu UN singur mediu disponibil (prod) -> header contine eticheta pentru Productie,
fara niciun control de toggle/comutare. fara niciun control de toggle/comutare.
Eticheta foloseste 'PRODUCȚIE' (diacritice + majuscule). Eticheta foloseste 'PRODUCȚIE' (diacritice + majuscule).
""" """
@@ -141,13 +143,14 @@ def test_afiseaza_env_default(client):
_seteaza_mediu_disponibil(acct_id, "prod") _seteaza_mediu_disponibil(acct_id, "prod")
_login(client, "env1@test.com", "parolasecreta10") _login(client, "env1@test.com", "parolasecreta10")
resp = client.get("/_fragments/status") resp = client.get("/")
assert resp.status_code == 200, f"Status HTTP neasteptat: {resp.status_code}" assert resp.status_code == 200, f"Status HTTP neasteptat: {resp.status_code}"
html = resp.text html = resp.text
assert 'id="header-env-badge"' in html, f"Badge mediu absent din header: {html[:800]}"
# Eticheta mediului trebuie prezenta — "PRODUC" acopera atat 'PRODUCȚIE' cat si # Eticheta mediului trebuie prezenta — "PRODUC" acopera atat 'PRODUCȚIE' cat si
# formele anterioare 'Productie'/'PRODUCTIE' (anti-regresie). # formele anterioare 'Productie'/'PRODUCTIE' (anti-regresie).
assert "PRODUC" in html.upper(), f"Eticheta Productie absenta in statusbar: {html[:800]}" assert "PRODUC" in html.upper(), f"Eticheta Productie absenta din badge: {html[:800]}"
# Nu trebuie sa existe un control de comutare (toggle) la un singur mediu # Nu trebuie sa existe un control de comutare (toggle) la un singur mediu
assert "toggle-env" not in html, \ assert "toggle-env" not in html, \
@@ -155,7 +158,7 @@ def test_afiseaza_env_default(client):
def test_toggle_doar_la_doua_medii(client): def test_toggle_doar_la_doua_medii(client):
"""Cont cu DOUA medii disponibile -> statusbar contine controlul de toggle. """Cont cu DOUA medii disponibile -> header contine controlul de toggle.
Cont cu UN singur mediu -> NU contine toggle. Cont cu UN singur mediu -> NU contine toggle.
""" """
# Cont cu doua medii # Cont cu doua medii
@@ -164,7 +167,7 @@ def test_toggle_doar_la_doua_medii(client):
_seteaza_mediu_disponibil(acct2_id, "prod") _seteaza_mediu_disponibil(acct2_id, "prod")
_login(client, "env2@test.com", "parolasecreta10") _login(client, "env2@test.com", "parolasecreta10")
resp2 = client.get("/_fragments/status") resp2 = client.get("/")
assert resp2.status_code == 200 assert resp2.status_code == 200
html2 = resp2.text html2 = resp2.text
assert "toggle-env" in html2, \ assert "toggle-env" in html2, \
@@ -176,7 +179,7 @@ def test_toggle_doar_la_doua_medii(client):
# Relogin pe alt cont (simulam logout/login rapid prin schimbare sesiune) # Relogin pe alt cont (simulam logout/login rapid prin schimbare sesiune)
_login(client, "env1b@test.com", "parolasecreta10") _login(client, "env1b@test.com", "parolasecreta10")
resp1 = client.get("/_fragments/status") resp1 = client.get("/")
assert resp1.status_code == 200 assert resp1.status_code == 200
html1 = resp1.text html1 = resp1.text
assert "toggle-env" not in html1, \ assert "toggle-env" not in html1, \
@@ -187,7 +190,7 @@ def test_toggle_doar_la_doua_medii(client):
def test_toggle_schimba_default(client): def test_toggle_schimba_default(client):
"""POST la /_fragments/status/toggle-env comuta rar_env_default in DB """POST la /_fragments/status/toggle-env comuta rar_env_default in DB
si intoarce statusbar-ul actualizat (200). si intoarce badge-ul actualizat (tinta hx-target) + status bar-ul ca OOB swap (200).
""" """
acct_id, _ = _create_account_user("Firma Toggle", "toggle@test.com") acct_id, _ = _create_account_user("Firma Toggle", "toggle@test.com")
_seteaza_mediu_disponibil(acct_id, "test") _seteaza_mediu_disponibil(acct_id, "test")
@@ -208,15 +211,17 @@ def test_toggle_schimba_default(client):
env_inainte = row_inainte["rar_env_default"] or "prod" env_inainte = row_inainte["rar_env_default"] or "prod"
# Obtine CSRF si face toggle # Obtine CSRF si face toggle
csrf = _get_csrf_din_status(client) csrf = _get_csrf_din_pagina(client)
resp = client.post("/_fragments/status/toggle-env", data={"csrf_token": csrf}) resp = client.post("/_fragments/status/toggle-env", data={"csrf_token": csrf})
assert resp.status_code == 200, \ assert resp.status_code == 200, \
f"Toggle a returnat {resp.status_code}: {resp.text[:300]}" f"Toggle a returnat {resp.status_code}: {resp.text[:300]}"
# Verifica ca statusbar-ul actualizat e in raspuns # Raspunsul contine badge-ul (tinta hx-target) + status bar-ul ca OOB swap
html = resp.text html = resp.text
assert "status-bar" in html, \ assert 'id="header-env-badge"' in html, \
f"Raspunsul toggle nu contine statusbar: {html[:400]}" f"Raspunsul toggle nu contine badge-ul din header: {html[:400]}"
assert "status-bar" in html and "hx-swap-oob" in html, \
f"Raspunsul toggle nu contine status bar-ul ca OOB swap: {html[:400]}"
# Verifica schimbarea in DB # Verifica schimbarea in DB
conn = get_connection() conn = get_connection()
@@ -232,3 +237,30 @@ def test_toggle_schimba_default(client):
f"rar_env_default NU s-a schimbat: inainte={env_inainte!r}, dupa={env_dupa!r}" f"rar_env_default NU s-a schimbat: inainte={env_inainte!r}, dupa={env_dupa!r}"
assert env_dupa in ("test", "prod"), \ assert env_dupa in ("test", "prod"), \
f"rar_env_default are valoare invalida: {env_dupa!r}" f"rar_env_default are valoare invalida: {env_dupa!r}"
def test_rar_medii_actualizeaza_badge_oob(client):
"""POST /cont/rar-medii cu un nou mediu implicit anexeaza badge-ul din header
ca OOB swap, ca header-ul sa reflecte imediat schimbarea (fara reload).
"""
acct_id, _ = _create_account_user("Firma Setari", "setari@test.com")
_seteaza_mediu_disponibil(acct_id, "test")
_seteaza_mediu_disponibil(acct_id, "prod")
_login(client, "setari@test.com", "parolasecreta10")
csrf = _get_csrf_din_pagina(client)
resp = client.post(
"/cont/rar-medii",
data={
"csrf_token": csrf,
"test_enabled": "1",
"prod_enabled": "1",
"rar_env_default": "test",
},
)
assert resp.status_code == 200, \
f"/cont/rar-medii a returnat {resp.status_code}: {resp.text[:400]}"
html = resp.text
assert 'id="header-env-badge"' in html and "hx-swap-oob" in html, \
f"Raspunsul /cont/rar-medii nu anexeaza badge-ul OOB din header: {html[:600]}"