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()

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>
</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 ===
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; }
.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">
<span class="rar-dot live" aria-hidden="true"></span>
<span class="rar-tx">RAR online</span>
<span class="rar-tx">RAR</span>
</div>
{% else %}
<div class="rar-chip rar-err" role="status"
@@ -966,6 +975,7 @@
<span class="rar-tx">RAR blocat</span>
</div>
{% 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' %}

View File

@@ -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]}"