diff --git a/app/web/routes.py b/app/web/routes.py index f66e19a..e3fa171 100644 --- a/app/web/routes.py +++ b/app/web/routes.py @@ -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'
' + f'Propunere: {escape(cod)}' + f'{" — " + str(escape(nume)) if nume else ""}' + f'{f" ({sursa})" if sursa else ""}' + f'
' + ) + 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='Nicio potrivire acum.' + content=sugestie_html + + 'Nicio potrivire acum.' ) exemple = ", ".join( @@ -2868,7 +2905,8 @@ def post_preview_regula_text( plural = "operatie nemapata" if n == 1 else "operatii nemapate" return HTMLResponse( content=( - f'' + sugestie_html + + f'' f'Potriveste {n} {plural}: {exemple}.' ) ) @@ -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() diff --git a/app/web/templates/_header_env_badge.html b/app/web/templates/_header_env_badge.html new file mode 100644 index 0000000..4dcb5a7 --- /dev/null +++ b/app/web/templates/_header_env_badge.html @@ -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([]) %} +
+ {% 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 %} + + {% else %} + + {{ _eb[0] }} + {{ _scurt }} + + {% endif %} + {% endif %} +
diff --git a/app/web/templates/_status.html b/app/web/templates/_status.html index 468629c..47b0cd2 100644 --- a/app/web/templates/_status.html +++ b/app/web/templates/_status.html @@ -156,43 +156,6 @@ - {# === 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 %} -
- Mediu RAR: - {{ _eb[0] }} - {% if _medii | length >= 2 %} - {# Toggle: apare DOAR cand sunt cel putin 2 medii disponibile #} -
- - -
- {% endif %} -
- {% endif %} - {# === Navigatie rapida: Trimiteri + Mapari cu badge needs_mapping === Pastrata exact ca inainte: tab_activ determina marcajul activ. #} diff --git a/app/web/templates/base.html b/app/web/templates/base.html index 8e2d734..3a9e1cd 100644 --- a/app/web/templates/base.html +++ b/app/web/templates/base.html @@ -150,6 +150,15 @@ 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; 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); } /* Heading/eticheta accesibila doar pentru cititoare de ecran (vizual ascunsa). */ .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 %}" aria-label="RAR online"> - RAR online + RAR {% else %}
RAR blocat
{% endif %} + {% include '_header_env_badge.html' %} {# Comutatorul "Trimite automat la RAR" langa semnalul RAR. In afara swap-ului periodic al #status-bar. #} {% include '_auto_send_toggle.html' %} diff --git a/tests/test_statusbar_env.py b/tests/test_statusbar_env.py index af19d26..7496db7 100644 --- a/tests/test_statusbar_env.py +++ b/tests/test_statusbar_env.py @@ -1,13 +1,15 @@ -"""Indicator mediu RAR in statusbar + toggle conditionat. +"""Badge mediu RAR compact in header + toggle conditionat. 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 /cont/rar-medii -- schimbare mediu implicit din Setari Teste: 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_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 @@ -116,14 +118,14 @@ def _seteaza_mediu_disponibil(acct_id: int, env: str) -> None: conn.close() -def _get_csrf_din_status(client) -> str: - """Obtine CSRF token din fragmentul /_fragments/status.""" - resp = client.get("/_fragments/status") - assert resp.status_code == 200, f"/_fragments/status a returnat {resp.status_code}" +def _get_csrf_din_pagina(client) -> str: + """Obtine CSRF token din pagina completa `/` (header, randat o singura data).""" + resp = client.get("/") + assert resp.status_code == 200, f"/ a returnat {resp.status_code}" m = re.search(r'name="csrf_token"\s+value="([^"]+)"', resp.text) if not m: 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) @@ -133,7 +135,7 @@ def _get_csrf_din_status(client) -> str: 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. Eticheta foloseste 'PRODUCȚIE' (diacritice + majuscule). """ @@ -141,13 +143,14 @@ def test_afiseaza_env_default(client): _seteaza_mediu_disponibil(acct_id, "prod") _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}" 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 # 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 assert "toggle-env" not in html, \ @@ -155,7 +158,7 @@ def test_afiseaza_env_default(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 doua medii @@ -164,7 +167,7 @@ def test_toggle_doar_la_doua_medii(client): _seteaza_mediu_disponibil(acct2_id, "prod") _login(client, "env2@test.com", "parolasecreta10") - resp2 = client.get("/_fragments/status") + resp2 = client.get("/") assert resp2.status_code == 200 html2 = resp2.text 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) _login(client, "env1b@test.com", "parolasecreta10") - resp1 = client.get("/_fragments/status") + resp1 = client.get("/") assert resp1.status_code == 200 html1 = resp1.text assert "toggle-env" not in html1, \ @@ -187,7 +190,7 @@ def test_toggle_doar_la_doua_medii(client): def test_toggle_schimba_default(client): """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") _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" # 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}) assert resp.status_code == 200, \ 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 - assert "status-bar" in html, \ - f"Raspunsul toggle nu contine statusbar: {html[:400]}" + assert 'id="header-env-badge"' in html, \ + 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 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}" assert env_dupa in ("test", "prod"), \ 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]}"