feat: UI polish - dark default, version drift vs default release, humanized enums, failed-jobs layout, processing button hierarchy, PT-BR titles/CTAs
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
from pathlib import Path
|
||||
import re
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
@@ -85,7 +86,7 @@ def test_dashboard_renders_operational_queue_labels_and_human_dates():
|
||||
assert "Produtos com blockers abertos" in response.text
|
||||
assert "Blockers abertos" in response.text
|
||||
assert "Atenção necessária" in response.text
|
||||
assert "Ver todos" in response.text
|
||||
assert "Ver blockers" in response.text
|
||||
assert "Jobs com falha" in response.text
|
||||
assert "Agents desatualizados" in response.text
|
||||
assert not re.search(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}", response.text)
|
||||
@@ -188,9 +189,9 @@ def test_dashboard_lists_bounded_failed_job_rows_with_identity_error_age_and_emp
|
||||
response = client.get("/")
|
||||
assert response.status_code == 200
|
||||
assert "Acme" in response.text and "Acme ERP" in response.text and "srv-01" in response.text
|
||||
assert "erro curto" in response.text and "Tipo / erro" in response.text and "Falhou" in response.text
|
||||
assert "erro curto" in response.text and "Falhas recentes" in response.text and "Falhou" in response.text
|
||||
assert 'href="/jobs?scope=active&status=FAILED&job_id=' in response.text
|
||||
assert response.text.count("erro curto") == 10
|
||||
assert response.text.count('class="error-text job-error-summary"') == 10 # bounded, one clamped summary per row
|
||||
assert 'title="2026-09-22T18:05:11+00:00"' in response.text
|
||||
assert ">22/09/2026 15:05<" in response.text
|
||||
with factory() as session:
|
||||
@@ -458,7 +459,8 @@ def test_agent_detail_reconciles_coverage_and_formats_missing_technical_fields()
|
||||
assert "Desconhecida · Desconhecida" not in response.text
|
||||
assert "ODS -" not in response.text
|
||||
context = response.text.split("<h2>Contexto da empresa</h2>", 1)[1].split("</article>", 1)[0]
|
||||
assert "Reprocessar tudo" in context
|
||||
assert "Reprocessar tudo" not in context # the only reprocess action lives in the Processamento panel
|
||||
assert response.text.count("Reprocessar tudo") == 1
|
||||
assert not re.search(r"<(?:a|button)\b[^>]*>\s*</(?:a|button)>", context)
|
||||
|
||||
|
||||
@@ -502,3 +504,35 @@ def test_agent_event_filters_lookahead_and_safe_event_reference_links_and_confir
|
||||
assert f'href="/agents/{agent_id}"' in confirmation.text
|
||||
assert client.post(f"/agents/{agent_id}/reprocess", data={"csrf_token": "wrong", "confirm": "yes"}).status_code == 403
|
||||
assert client.post(f"/agents/{agent_id}/reprocess", data={"csrf_token": csrf, "confirm": "yes"}, follow_redirects=False).status_code == 303
|
||||
|
||||
|
||||
def test_theme_defaults_to_dark_and_never_overrides_a_saved_choice():
|
||||
client, _, _ = _panel_client()
|
||||
base = client.get("/agents").text
|
||||
# no saved preference -> dark; an explicit "claro" is the only thing that yields light
|
||||
assert 'tema === "claro" ? "claro" : "escuro"' in base
|
||||
assert "prefers-color-scheme" not in (Path(__file__).parents[1] / "static" / "tema.js").read_text(encoding="utf-8")
|
||||
script = (Path(__file__).parents[1] / "static" / "tema.js").read_text(encoding="utf-8")
|
||||
assert 'return "escuro"' in script and 'localStorage.setItem("tema", novo)' in script
|
||||
for page in ("login.html", "logged_out.html", "sso_error.html"):
|
||||
assert 'data-tema="escuro"' in (Path(__file__).parents[1] / "templates" / page).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_operator_pages_use_portuguese_titles_and_consistent_ctas():
|
||||
client, agent_id, _ = _panel_client()
|
||||
assert "<title>Agents / Servidores | GTI Fiscal</title>" in client.get("/agents").text
|
||||
assert "<title>Blockers | GTI Fiscal</title>" in client.get("/blockers").text
|
||||
assert "<title>Jobs | GTI Fiscal</title>" in client.get("/jobs").text
|
||||
dashboard = client.get("/").text
|
||||
for old in ("Abrir fila", "Ver instalações", "Ver todos", "Ver job #"):
|
||||
assert old not in dashboard
|
||||
|
||||
|
||||
def test_agent_detail_processing_hierarchy_and_status_badges():
|
||||
client, agent_id, _ = _panel_client()
|
||||
page = client.get(f"/agents/{agent_id}").text
|
||||
assert page.count("Reprocessar tudo") == 1
|
||||
assert 'class="primary-action"' in page and 'class="botao secondary"' in page and 'class="caution"' in page
|
||||
assert "Mantém o heartbeat, mas não executa jobs" in page and "Recalcula todos os produtos" in page
|
||||
assert "Saúde técnica: " not in page and "Status fiscal: " not in page # context lives in the small label, not in the badge
|
||||
assert '<small class="status-label">Saúde técnica</small>' in page
|
||||
|
||||
@@ -564,3 +564,13 @@ def test_blocker_breakdown_query_groups_on_same_postgres_expression():
|
||||
|
||||
assert "GROUP BY blocked_products.field" in sql
|
||||
assert "GROUP BY coalesce" not in sql
|
||||
|
||||
|
||||
def test_remaining_technical_enums_have_human_labels():
|
||||
from central.app.panel_presentation import health_reason_label
|
||||
assert blocker_reason_label("INVALID_NCM") == "NCM inválido"
|
||||
assert blocker_reason_label("LEGACY_NCM") == "NCM legado"
|
||||
assert blocker_type_label("BLOCKED_MISSING_EVIDENCE") == "Evidência fiscal ausente"
|
||||
assert health_reason_label("last heartbeat exceeds 300s") == "Sem heartbeat recente"
|
||||
assert health_reason_label("persistent technical failure") == "Falha técnica persistente"
|
||||
assert blocker_reason_label("SOMETHING_NEW") == "Something new" # unknown codes degrade readably, never crash
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
"""'Atualização disponível' must compare the Agent with the real default release, semantically."""
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from central.app.control_plane import dashboard_summary, list_installations
|
||||
from central.app.installation_identity import compute_version_drift, outdated_versions, parse_version, reference_agent_version
|
||||
from central.app.models import AgentRelease
|
||||
from central.tests.test_control_plane import setup_db
|
||||
from central.tests.test_operational_view_models import NOW, _installation
|
||||
|
||||
|
||||
def _release(session, version, status="PUBLISHED", platform="windows"):
|
||||
session.add(AgentRelease(version=version, platform=platform, architecture="x64", filename=f"GTIFiscalAgent-Setup-{version}.exe", storage_path=f"{version}.exe",
|
||||
size_bytes=1, sha256=version.ljust(64, "0")[:64], status=status))
|
||||
session.flush()
|
||||
|
||||
|
||||
def test_versions_compare_semantically_not_as_strings():
|
||||
assert compute_version_drift("1.1.7", "1.1.7") == "CURRENT"
|
||||
assert compute_version_drift("1.1.10", "1.1.9") == "CURRENT" # string compare would say older
|
||||
assert compute_version_drift("1.1.6", "1.1.7") == "OUTDATED"
|
||||
assert compute_version_drift("1.2.0", "1.1.7") == "CURRENT" # ahead of the release is not "outdated"
|
||||
assert compute_version_drift("1.0.0rc1", "1.0.0") == "OUTDATED" # pre-release sorts below its final
|
||||
assert compute_version_drift(None, "1.1.7") == "UNKNOWN"
|
||||
assert parse_version("garbage") is None
|
||||
|
||||
|
||||
def test_reference_is_the_highest_published_release_and_ignores_revoked_and_drafts():
|
||||
with setup_db()() as session:
|
||||
_release(session, "1.1.5", "REVOKED")
|
||||
_release(session, "1.1.6")
|
||||
_release(session, "1.1.7")
|
||||
_release(session, "1.9.9", "REVOKED")
|
||||
_release(session, "2.0.0", "DRAFT")
|
||||
_release(session, "3.0.0", platform="linux")
|
||||
assert reference_agent_version(session) == "1.1.7"
|
||||
|
||||
|
||||
def test_agent_on_the_default_release_is_current_everywhere_even_with_a_stale_snapshot():
|
||||
with setup_db()() as session:
|
||||
_release(session, "1.1.7")
|
||||
_release(session, "1.1.6", "REVOKED")
|
||||
# snapshot was written as OUTDATED by an older heartbeat (compared with a stale reference)
|
||||
_installation(session, 1, version="1.1.7")
|
||||
_installation(session, 2, version="1.1.6")
|
||||
session.commit()
|
||||
assert reference_agent_version(session) == "1.1.7"
|
||||
assert outdated_versions(session, "1.1.7") == ["1.1.6"]
|
||||
rows = {item["agent_version"]: item for item in list_installations(session, now=NOW)["items"]}
|
||||
assert rows["1.1.7"]["version_drift"] == "CURRENT" and rows["1.1.6"]["version_drift"] == "OUTDATED"
|
||||
assert [i["agent_version"] for i in list_installations(session, version="outdated", now=NOW)["items"]] == ["1.1.6"]
|
||||
summary = dashboard_summary(session, now=NOW)
|
||||
assert [a["agent_version"] for a in summary["outdated_agents"]] == ["1.1.6"]
|
||||
|
||||
|
||||
def test_a_newer_published_release_makes_the_previous_one_outdated():
|
||||
with setup_db()() as session:
|
||||
_release(session, "1.1.7")
|
||||
_installation(session, 1, version="1.1.7")
|
||||
session.commit()
|
||||
assert list_installations(session, version="outdated", now=NOW)["total"] == 0
|
||||
_release(session, "1.1.8")
|
||||
session.commit()
|
||||
assert list_installations(session, version="outdated", now=NOW)["total"] == 1
|
||||
Reference in New Issue
Block a user