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:
gti
2026-09-24 14:26:44 -03:00
co-authored by Claude Sonnet 5
parent 9a142975a6
commit c950ffb47f
18 changed files with 247 additions and 52 deletions
+2 -2
View File
@@ -15,7 +15,7 @@ from .blocked_products import list_blockers, resolve_blocker, resolve_product_bl
from .context_keys import validate_entry
from .models import AgentSnapshot, BlockedProduct, FiscalContextEntry, ClassificationCache, ClassificationRequest, ClassificationResult, Dataset, Installation, Job
from .events import record_event
from .installation_identity import compute_agent_health, compute_version_drift, sanitize_registration
from .installation_identity import compute_agent_health, compute_version_drift, reference_agent_version, sanitize_registration
from .control_plane import list_jobs
from .human_auth import require_human
@@ -201,7 +201,7 @@ def router(session_factory, bootstrap_token):
snapshot.failed_products = int(coverage.get("failed_products", snapshot.failed_products) or 0)
snapshot.coverage_percent = float(coverage.get("coverage_percent", snapshot.coverage_percent) or 0)
snapshot.blockers = coverage.get("blockers", snapshot.blockers) or {}
snapshot.version_drift = compute_version_drift(agent.agent_version)
snapshot.version_drift = compute_version_drift(agent.agent_version, reference_agent_version(session))
snapshot.agent_health, snapshot.health_reason = compute_agent_health(installation, supplemental=snapshot)
session.add(agent); session.flush()
record_event(session, tenant_id=agent.tenant_id, installation_id=installation.id, agent_id=agent.id, severity="INFO", event_type="HEARTBEAT", message="Agent heartbeat", context={"status": agent.last_status, "agent_version": agent.agent_version})
+2 -1
View File
@@ -7,6 +7,7 @@ from sqlalchemy import select
from .audit import record_audit
from .blocker_resolution import BlockerResolutionUnavailable, resolve_blocker_with_context
from .installation_identity import reference_agent_version
from .control_plane import dashboard_summary, installation_view, list_blockers, list_installations, list_jobs, list_panel_events
from .human_auth import require_csrf, require_human
from .models import Agent, AgentSnapshot, BlockedProduct, Installation, Job, Tenant
@@ -46,7 +47,7 @@ def router(session_factory):
session, _, _ = identity
try:
installation, agent, tenant, snapshot = get_installation(session, agent_id)
return installation_view(installation, agent, tenant, snapshot)
return installation_view(installation, agent, tenant, snapshot, reference=reference_agent_version(session))
finally: session.close()
@routes.get("/agents/{agent_id}/health")
+15 -13
View File
@@ -10,9 +10,9 @@ from sqlalchemy import Numeric, and_, case, cast, func, or_, select
from .config import settings
from .events import list_events
from .installation_identity import compute_agent_health, compute_version_drift
from .installation_identity import compute_agent_health, compute_version_drift, outdated_versions, reference_agent_version
from .models import Agent, AgentEnrollment, AgentEvent, AgentSnapshot, AuditEvent, BlockedProduct, Installation, Job, Tenant
from .panel_presentation import blocker_reason_label, blocker_type_label, fiscal_status_label, format_age, format_duration, format_ui_datetime, health_label, job_kind_label, job_status_label, operational_priority, priority_label
from .panel_presentation import health_reason_label, blocker_reason_label, blocker_type_label, fiscal_status_label, format_age, format_duration, format_ui_datetime, health_label, job_kind_label, job_status_label, operational_priority, priority_label
HEALTH_STATES = ("ONLINE", "DEGRADED", "OFFLINE", "FAILED", "UNKNOWN")
@@ -131,13 +131,13 @@ def _attention_item(item, problem, timestamp, *, rank, link=None, now=None):
}
def installation_view(installation, agent, tenant, snapshot=None, *, now=None):
def installation_view(installation, agent, tenant, snapshot=None, *, now=None, reference=None):
has_snapshot = snapshot is not None
snapshot = snapshot or AgentSnapshot(installation_id=installation.id)
health, reason = compute_agent_health(installation, now=now, supplemental=snapshot)
version_drift = snapshot.version_drift
if not version_drift or version_drift == "UNKNOWN":
version_drift = compute_version_drift(agent.agent_version)
# Always recomputed against the current reference release: the value stored on the snapshot was taken at the
# last heartbeat (against whatever was "current" then) and goes stale as soon as a newer release is published.
version_drift = compute_version_drift(agent.agent_version, reference)
fiscal_status = _fiscal_status(snapshot)
coverage_partition = _coverage_partition(snapshot)
if not has_snapshot:
@@ -213,6 +213,7 @@ def _installation_query_filters(query, *, client=None, version=None, search=None
def list_installations(session, *, page=1, page_size=50, client=None, health=None, version=None, search=None, last_seen_before=None, sort="display_name", scope="active", now=None):
page, page_size = _page(page, page_size)
now = _utc_now(now)
reference = reference_agent_version(session)
query = select(Installation, Agent, Tenant, AgentSnapshot, operational_installation_predicate().label("operational_active")).join(Agent, Agent.id == Installation.agent_id).join(Tenant, Tenant.id == Installation.tenant_id).outerjoin(AgentSnapshot, AgentSnapshot.installation_id == Installation.id)
if scope == "active":
query = query.where(operational_installation_predicate())
@@ -220,7 +221,7 @@ def list_installations(session, *, page=1, page_size=50, client=None, health=Non
raise ValueError("scope must be active or all")
query = _installation_query_filters(query, client=client, version=version, search=search, last_seen_before=last_seen_before)
if version == "outdated":
query = query.where(or_(AgentSnapshot.version_drift == "OUTDATED", and_(Agent.agent_version.is_not(None), Agent.agent_version != settings.agent_server_version)))
query = query.where(Agent.agent_version.in_(outdated_versions(session, reference)))
health_expression = _health_expression(now)
if health:
query = query.where(health_expression == health)
@@ -229,7 +230,7 @@ def list_installations(session, *, page=1, page_size=50, client=None, health=Non
else:
ordered = query.order_by(Installation.display_name.asc(), Installation.id.asc())
rows = session.execute(ordered.offset((page - 1) * page_size).limit(page_size)).all()
items = [{**installation_view(i, a, t, s, now=now), "operational_active": bool(active)} for i, a, t, s, active in rows]
items = [{**installation_view(i, a, t, s, now=now, reference=reference), "operational_active": bool(active)} for i, a, t, s, active in rows]
count_query = select(func.count()).select_from(query.with_only_columns(Installation.id).order_by(None).subquery())
total = session.scalar(count_query) or 0
return {"page": page, "page_size": page_size, "items": items, "total": total, "health_filter_applied": health}
@@ -430,6 +431,7 @@ def jobs_status_summary(session, *, scope="active"):
def dashboard_summary(session, now=None):
now = _utc_now(now)
reference = reference_agent_version(session)
active = operational_installation_predicate()
installation_query = select(Installation, Agent, Tenant, AgentSnapshot).join(Agent, Agent.id == Installation.agent_id).join(Tenant, Tenant.id == Installation.tenant_id).outerjoin(AgentSnapshot, AgentSnapshot.installation_id == Installation.id).where(active)
health_expression = _health_expression(now)
@@ -483,12 +485,12 @@ def dashboard_summary(session, now=None):
def installation_candidates(health):
query = installation_query.where(health_expression == health).order_by(Installation.id.asc()).limit(ATTENTION_CANDIDATE_LIMIT)
return [installation_view(i, a, t, s, now=now) for i, a, t, s in session.execute(query).all()]
return [installation_view(i, a, t, s, now=now, reference=reference) for i, a, t, s in session.execute(query).all()]
health_candidates = {health: installation_candidates(health) for health in ("FAILED", "DEGRADED", "OFFLINE", "UNKNOWN")}
version_filter = or_(AgentSnapshot.version_drift == "OUTDATED", and_(Agent.agent_version.is_not(None), Agent.agent_version != settings.agent_server_version))
version_filter = Agent.agent_version.in_(outdated_versions(session, reference))
outdated_query = installation_query.where(version_filter).order_by(Installation.id.asc()).limit(ATTENTION_CANDIDATE_LIMIT)
outdated_agents = [installation_view(i, a, t, s, now=now) for i, a, t, s in session.execute(outdated_query).all()]
outdated_agents = [installation_view(i, a, t, s, now=now, reference=reference) for i, a, t, s in session.execute(outdated_query).all()]
group_field = func.upper(func.coalesce(BlockedProduct.field, "OTHER"))
fiscal_priority = case(
@@ -512,13 +514,13 @@ def dashboard_summary(session, now=None):
attention = []
for health in ("FAILED", "DEGRADED", "OFFLINE", "UNKNOWN"):
rank = {"FAILED": 0, "DEGRADED": 1, "OFFLINE": 2, "UNKNOWN": 3}[health]
attention.extend((_attention_item(item, item["health_reason"], _technical_timestamp(item), rank=rank, now=now), rank, item["agent_id"]) for item in health_candidates[health])
attention.extend((_attention_item(item, health_reason_label(item["health_reason"]), _technical_timestamp(item), rank=rank, now=now), rank, item["agent_id"]) for item in health_candidates[health])
for client_name, display_name, hostname, agent_id, field, blocker_type, reason, count, products, last_seen, in blocker_groups:
item = {"client": client_name, "display_name": display_name, "hostname": hostname, "agent_id": agent_id, "technical_health": "BLOCKED"}
link = "/blockers?" + urlencode({"scope": "active", "status": "OPEN", "agent_id": agent_id, "field": field, "blocker_type": blocker_type, "reason_code": reason})
priority = operational_priority({"field": field, "blocker_type": blocker_type, "reason_code": reason})
rank = {"CRITICAL": 3, "HIGH": 4, "NORMAL": 5, "LOW": 6}[priority]
entry = _attention_item(item, f"{field}: {reason}", last_seen, rank=rank, link=link, now=now)
entry = _attention_item(item, f"{field}: {blocker_reason_label(reason)}", last_seen, rank=rank, link=link, now=now)
entry.update(group_count=count, products_affected=products, field=field, priority=priority)
attention.append((entry, rank, agent_id))
for item in outdated_agents:
+46 -4
View File
@@ -94,11 +94,53 @@ def compute_agent_health(snapshot, now: datetime | None = None, *, supplemental=
return "ONLINE", "recent heartbeat without critical technical failure"
def compute_version_drift(agent_version: str | None) -> str:
_VERSION_RE = re.compile(r"^v?(\d+(?:\.\d+)*)(?:[-_.]?(rc|beta|alpha|b|a)[-_.]?(\d*))?$", re.IGNORECASE)
_PRE_RANK = {"alpha": -3, "a": -3, "beta": -2, "b": -2, "rc": -1}
def parse_version(value: str | None):
"""Comparable tuple for '1.1.7', '1.0.0rc1', ... (a pre-release sorts below its final); None if unparsable."""
match = _VERSION_RE.match((value or "").strip())
if not match:
return None
parts = [int(x) for x in match.group(1).split(".")]
while len(parts) < 4:
parts.append(0)
pre = (_PRE_RANK[match.group(2).lower()], int(match.group(3) or 0)) if match.group(2) else (0, 0)
return (*parts, *pre)
def reference_agent_version(session) -> str:
"""The version an Agent should be running: the highest PUBLISHED Windows Agent release (REVOKED/DRAFT are
never a target). Falls back to the configured server version when no release is published."""
from sqlalchemy import select
from .models import AgentRelease
best, best_key = None, None
for version in session.scalars(select(AgentRelease.version).where(AgentRelease.status == "PUBLISHED", AgentRelease.platform == "windows")):
key = parse_version(version)
if key is not None and (best_key is None or key > best_key):
best, best_key = version, key
return best or settings.agent_server_version
def compute_version_drift(agent_version: str | None, reference: str | None = None) -> str:
"""CURRENT when the Agent is at (or ahead of) the reference release, OUTDATED when behind it."""
if not agent_version:
return "UNKNOWN"
return "CURRENT" if agent_version == settings.agent_server_version else "OUTDATED"
reference = reference or settings.agent_server_version
have, want = parse_version(agent_version), parse_version(reference)
if have is None or want is None:
return "CURRENT" if agent_version == reference else "OUTDATED"
return "CURRENT" if have >= want else "OUTDATED"
def machine_fingerprint(parts: list[str]) -> str:
return hashlib.sha256("|".join(parts).encode()).hexdigest()
def outdated_versions(session, reference: str) -> list[str]:
"""Distinct Agent versions in use that are behind the reference (used to keep SQL filters exact)."""
from sqlalchemy import select
from .models import Agent
used = [v for v in session.scalars(select(Agent.agent_version).where(Agent.agent_version.is_not(None)).distinct())]
return [v for v in used if compute_version_drift(v, reference) == "OUTDATED"]
+22 -2
View File
@@ -121,11 +121,31 @@ def job_kind_label(value):
def blocker_type_label(value):
return _human_label(value, {"BLOCKED_MISSING_EVIDENCE": "Evidência fiscal ausente", "MISSING_EVIDENCE": "Evidência fiscal ausente", "TECHNICAL_FAILURE": "Falha técnica", "CONNECTION_ERROR": "Erro de conexão"})
return _human_label(value, {"BLOCKED_MISSING_EVIDENCE": "Evidência fiscal ausente", "MISSING_EVIDENCE": "Evidência fiscal ausente", "TECHNICAL_FAILURE": "Falha técnica", "CONNECTION_ERROR": "Erro de conexão", "OTHER": "Outro"})
_REASON_LABELS = {
"INVALID_NCM": "NCM inválido", "LEGACY_NCM": "NCM legado", "LEGACY_NCM_NO_AUTOMATIC_SUCCESSOR": "NCM legado sem sucessor automático",
"LEGACY_NCM_REVIEW": "NCM legado em revisão", "LEGACY_NCM_REVIEW_REQUIRED": "NCM legado exige revisão", "LEGACY_OR_INVALID_NCM": "NCM legado ou inválido",
"INSUFFICIENT_EVIDENCE": "Evidência insuficiente", "MISSING_EVIDENCE": "Evidência fiscal ausente", "BLOCKED_MISSING_EVIDENCE": "Evidência fiscal ausente",
"MISSING_CONTEXT": "Contexto fiscal ausente", "NO_EVIDENCE": "Sem evidência", "MISSING_PRODUCT_ORIGIN": "Origem do produto ausente",
"MISSING_PROFILE": "Perfil fiscal ausente", "MISSING_ST_DETERMINATION": "Determinação de ST ausente", "NO_IPI_DECISION": "Sem decisão de IPI",
"NO_NUMERIC_VALUE": "Sem valor numérico", "CHANGE_REQUIRED": "Alteração necessária", "SAME_VALUE": "Valor já correto", "AMBIGUOUS": "Ambíguo",
}
def blocker_reason_label(value):
return _human_label(value, {"INVALID_NCM": "NCM inválido", "INSUFFICIENT_EVIDENCE": "Evidência insuficiente", "MISSING_CONTEXT": "Contexto fiscal ausente", "NO_EVIDENCE": "Sem evidência"})
return _human_label(value, _REASON_LABELS)
def health_reason_label(value):
"""Operator wording for the technical health reason strings produced by compute_agent_health."""
text = str(value or "")
if text.startswith("last heartbeat exceeds"):
return "Sem heartbeat recente"
return {"no heartbeat received": "Nenhum heartbeat recebido", "persistent technical failure": "Falha técnica persistente",
"recent technical or job failure": "Falha técnica ou de job recente",
"recent heartbeat without critical technical failure": "Operando normalmente"}.get(text, text or "Não informado")
def priority_label(value):
+4 -2
View File
@@ -14,6 +14,8 @@ from . import agent_distribution, client_sync, config, enrollment, sso
from .config import settings
from .blocker_resolution import BlockerResolutionUnavailable, _company_key, resolve_blocker_with_context
from .context_keys import ALLOWED_KEYS, ENUMS
from .installation_identity import reference_agent_version
from .panel_presentation import job_kind_label, job_status_label
from .control_plane import blocker_detail, blocker_filter_summary, dashboard_summary, installation_view, jobs_status_summary, list_blockers, list_installations, list_jobs, list_panel_events
from .human_auth import create_session, current_session, require_csrf, verify_password
from .panel_presentation import format_ui_datetime
@@ -275,7 +277,7 @@ def router(session_factory):
row = session.execute(select(Installation, Agent, Tenant, AgentSnapshot).join(Agent, Agent.id == Installation.agent_id).join(Tenant, Tenant.id == Installation.tenant_id).outerjoin(AgentSnapshot, AgentSnapshot.installation_id == Installation.id).where(Agent.id == agent_id)).first()
if row is None: return HTMLResponse("Agent not found", status_code=404)
installation, agent, tenant, snapshot = row
detail = installation_view(installation, agent, tenant, snapshot)
detail = installation_view(installation, agent, tenant, snapshot, reference=reference_agent_version(session))
event_page = max(1, event_page)
events = list_panel_events(session, installation.id, page=event_page, page_size=25, include_more=True, severity=severity, event_type=event_type, job_id=job_id, product_code=product_code, since=date_filter(since), until=date_filter(until, end_of_day=True))
has_more = len(events) > 25
@@ -295,7 +297,7 @@ def router(session_factory):
next_sync = last_sync + timedelta(seconds=installation.sync_interval_seconds)
return {"enabled": bool(installation.processing_enabled), "interval_minutes": installation.sync_interval_seconds // 60,
"last_sync": format_ui_datetime(last_sync) if last_sync else None, "next_sync": format_ui_datetime(next_sync) if next_sync else None,
"last_changed": installation.last_sync_changed, "last_job": {"kind": last_job.kind, "status": last_job.status, "id": last_job.id} if last_job else None}
"last_changed": installation.last_sync_changed, "last_job": {"kind": last_job.kind, "status": last_job.status, "id": last_job.id, "kind_label": job_kind_label(last_job.kind), "status_label": job_status_label(last_job.status)} if last_job else None}
async def _installation_action(request, agent_id, action):
session, auth_data, redirect = page_auth(request)