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([]) %}
+
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]}"