fix: reconcile final control panel coverage UX

This commit is contained in:
gti
2026-09-22 23:04:53 -03:00
parent 283749e860
commit ce606b04cc
12 changed files with 312 additions and 12 deletions
+35 -1
View File
@@ -76,6 +76,24 @@ def _fiscal_status(snapshot) -> str:
return "INCOMPLETE"
def _coverage_partition(snapshot) -> dict:
"""Expose only explicit snapshot states and label the remaining count honestly."""
total = int(getattr(snapshot, "total_products", 0) or 0)
complete = int(getattr(snapshot, "fiscally_complete", 0) or 0)
safe_blocked = int(getattr(snapshot, "safe_blocked", 0) or 0)
failed = int(getattr(snapshot, "failed_products", 0) or 0)
explicit = complete + safe_blocked + failed
residual = total - explicit
invalid_counts = min(total, complete, safe_blocked, failed) < 0
overcount = max(0, explicit - total)
return {
"unclassified_products": max(0, residual) if not invalid_counts else 0,
"coverage_overcount_products": overcount,
"coverage_invalid_counts": invalid_counts,
"coverage_invariant_pass": not invalid_counts and overcount == 0,
}
def _technical_timestamp(item):
if item["technical_health"] in {"FAILED", "DEGRADED"}:
return item.get("last_job_at") or item.get("last_seen_at")
@@ -105,6 +123,9 @@ def installation_view(installation, agent, tenant, snapshot=None, *, now=None):
if version_drift == "UNKNOWN":
version_drift = compute_version_drift(agent.agent_version)
fiscal_status = _fiscal_status(snapshot)
coverage_partition = _coverage_partition(snapshot)
version_drift_label = {"CURRENT": "Atualizada", "OUTDATED": "Desatualizada", "UNKNOWN": "Desconhecida"}.get(version_drift, str(version_drift))
agent_version_display = f"{agent.agent_version} · {version_drift_label}" if agent.agent_version else "Desconhecida"
return {
"agent_id": agent.id,
"installation_id": installation.installation_id,
@@ -119,7 +140,9 @@ def installation_view(installation, agent, tenant, snapshot=None, *, now=None):
"company_uf": installation.company_uf,
"company_crt": installation.company_crt,
"agent_version": agent.agent_version,
"agent_version_display": agent_version_display,
"version_drift": version_drift,
"version_drift_label": version_drift_label,
"health": health,
"health_reason": reason,
"technical_health": health,
@@ -134,6 +157,7 @@ def installation_view(installation, agent, tenant, snapshot=None, *, now=None):
"fiscally_complete": snapshot.fiscally_complete,
"safe_blocked": snapshot.safe_blocked,
"failed_products": snapshot.failed_products,
**coverage_partition,
"coverage_percent": snapshot.coverage_percent,
"fiscal_completion_percent": _fiscal_completion(snapshot),
"fiscal_status": fiscal_status,
@@ -362,15 +386,25 @@ def dashboard_summary(session, now=None):
for state, count in health_rows:
counts[state] = int(count)
snapshot_total = func.coalesce(AgentSnapshot.total_products, 0)
snapshot_complete = func.coalesce(AgentSnapshot.fiscally_complete, 0)
snapshot_blocked = func.coalesce(AgentSnapshot.safe_blocked, 0)
snapshot_failed = func.coalesce(AgentSnapshot.failed_products, 0)
snapshot_explicit = snapshot_complete + snapshot_blocked + snapshot_failed
snapshot_has_negative = or_(snapshot_total < 0, snapshot_complete < 0, snapshot_blocked < 0, snapshot_failed < 0)
total_row = session.execute(
select(
func.coalesce(func.sum(AgentSnapshot.total_products), 0),
func.coalesce(func.sum(AgentSnapshot.fiscally_complete), 0),
func.coalesce(func.sum(AgentSnapshot.safe_blocked), 0),
func.coalesce(func.sum(AgentSnapshot.failed_products), 0),
func.coalesce(func.sum(case((and_(~snapshot_has_negative, snapshot_explicit <= snapshot_total), snapshot_total - snapshot_explicit), else_=0)), 0),
func.coalesce(func.sum(case((and_(~snapshot_has_negative, snapshot_explicit > snapshot_total), snapshot_explicit - snapshot_total), else_=0)), 0),
func.coalesce(func.sum(case((snapshot_has_negative, 1), else_=0)), 0),
).select_from(Installation).outerjoin(AgentSnapshot, AgentSnapshot.installation_id == Installation.id)
).one()
totals = dict(zip(("total_products", "fiscally_complete", "safe_blocked", "failed_products"), (int(value or 0) for value in total_row)))
totals = dict(zip(("total_products", "fiscally_complete", "safe_blocked", "failed_products", "unclassified_products", "coverage_overcount_products", "coverage_invalid_snapshots"), (int(value or 0) for value in total_row)))
totals["coverage_invariant_pass"] = totals["coverage_overcount_products"] == 0 and totals["coverage_invalid_snapshots"] == 0
open_filter = BlockedProduct.resolved_at.is_(None)
open_blockers = session.scalar(select(func.count(BlockedProduct.id)).where(open_filter)) or 0
@@ -23,7 +23,12 @@ def generate_fixture(session, agent_count=100, seed=7):
last_seen = None if state == "UNKNOWN" else now - timedelta(seconds=10 if state in {"ONLINE", "DEGRADED"} else 10_000)
installation = Installation(agent_id=agent.id, tenant_id=tenant.id, installation_id=str(uuid.uuid5(uuid.NAMESPACE_DNS, f"fixture-{index}")), display_name=f"{tenant.name} - Servidor Principal", customer_code=f"F{index:03d}", hostname=f"srv-{index:03d}", company_name=f"Empresa {index:03d}", company_uf="BA", company_crt="1", last_seen_at=last_seen, last_status="IDLE")
session.add(installation); session.flush()
snapshot = AgentSnapshot(installation_id=installation.id, total_products=5000 + index, fiscally_complete=4900 + index, safe_blocked=1 if index % 2 == 0 else 0, failed_products=1 if state == "FAILED" else 0, coverage_percent=98.0, blockers={"CFOP": index % 7, "ST": index % 5}, agent_health=state, version_drift="UNKNOWN" if agent_version is None else ("OUTDATED" if agent_version != "1.0.0rc1" else "CURRENT"), last_job_status="FAILED" if state in {"DEGRADED", "FAILED"} else "COMPLETED")
total_products = 5000 + index
safe_blocked = 1 if index % 2 == 0 else 0
failed_products = 1 if state == "FAILED" else 0
fiscally_complete = total_products - safe_blocked - failed_products
coverage_percent = round(fiscally_complete * 100 / total_products, 1)
snapshot = AgentSnapshot(installation_id=installation.id, total_products=total_products, fiscally_complete=fiscally_complete, safe_blocked=safe_blocked, failed_products=failed_products, coverage_percent=coverage_percent, blockers={"CFOP": index % 7, "ST": index % 5}, agent_health=state, version_drift="UNKNOWN" if agent_version is None else ("OUTDATED" if agent_version != "1.0.0rc1" else "CURRENT"), last_job_status="FAILED" if state in {"DEGRADED", "FAILED"} else "COMPLETED")
session.add(snapshot); created["agents"] += 1
failed = state in {"DEGRADED", "FAILED"}
+12 -1
View File
@@ -43,10 +43,14 @@ small { display: block; font-size: 12px; }
.panel-title { display: flex; justify-content: space-between; align-items: center; gap: 12px; }
.panel-title h2 { margin-bottom: 0; }
.panel-title a { color: var(--accent); font-size: 12px; }
.coverage { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; }
.panel-title a.secondary { color: #fff; font-size: inherit; }
.coverage { display: grid; grid-template-columns: repeat(5, 1fr); gap: 12px; }
.coverage div { border-left: 3px solid var(--accent-soft); padding-left: 12px; }
.coverage strong { display: block; font-size: 23px; }
.coverage span { color: var(--muted); font-size: 12px; }
.coverage-status { margin: 16px 0 0; color: var(--accent); font-size: 12px; font-weight: 700; }
.registry-summary { display: flex; flex-wrap: wrap; gap: 16px; margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--line); color: var(--muted); font-size: 12px; }
.registry-summary a { text-decoration: none; }
.bar-row { display: flex; justify-content: space-between; border-bottom: 1px solid var(--line); padding: 8px 0; }
.bar-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0 24px; }
.list-row { display: flex; justify-content: space-between; align-items: center; gap: 12px; padding: 12px 0; border-bottom: 1px solid var(--line); text-decoration: none; color: inherit; }
@@ -90,6 +94,12 @@ td a { text-decoration: none; }
.event-row { display: grid; grid-template-columns: 120px 1fr auto; align-items: start; gap: 12px; padding: 11px 0; border-bottom: 1px solid var(--line); }
.event-row:last-child { border-bottom: 0; }
.event-time { color: var(--muted); font-size: 12px; }
.login-page { min-height: 100vh; display: grid; place-items: center; padding: 24px; background: var(--paper); }
.login-card { width: min(100%, 420px); display: grid; gap: 10px; padding: 30px; background: var(--card); border: 1px solid var(--line); border-radius: 14px; box-shadow: var(--shadow); }
.login-card .eyebrow, .login-card h1 { margin-bottom: 2px; }
.login-card label { margin-top: 6px; color: var(--muted); font-size: 13px; font-weight: 700; }
.login-card input { width: 100%; }
.login-card button { width: 100%; margin-top: 8px; }
.form-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 14px; }
.form-grid label { display: grid; gap: 5px; color: var(--muted); font-weight: 700; }
.form-grid .wide { grid-column: 1 / -1; }
@@ -120,4 +130,5 @@ td a { text-decoration: none; }
.cards, .cards.compact, .split, .bar-grid { grid-template-columns: 1fr; }
.filters label, .filters input, .filters select, .filters button { width: 100%; }
.filters button { margin-top: 2px; }
.login-card { padding: 22px; }
}
+3 -2
View File
@@ -2,9 +2,10 @@
{% block title %}{{ detail.display_name }} | GTI Fiscal{% endblock %}
{% block content %}
<div class="detail-head"><div><p class="eyebrow">{{ detail.client or "Cliente não identificado" }} / INSTALAÇÃO</p><h1>{{ detail.display_name or detail.hostname or detail.installation_id }}</h1><p class="muted">{{ detail.hostname or "hostname indisponível" }}</p></div><div class="badge-stack"><span class="badge {{ detail.technical_health|lower }} large">Saúde técnica: {{ detail.technical_health_label }}</span><span class="badge fiscal-{{ detail.fiscal_status|lower }} large">Status fiscal: {{ detail.fiscal_status_label }}</span></div></div>
<section class="cards compact"><article class="metric"><span>Produtos</span><strong>{{ detail.total_products }}</strong></article><article class="metric"><span>Completos</span><strong>{{ detail.fiscally_complete }}</strong></article><article class="metric"><span>Bloqueados com segurança</span><strong>{{ detail.safe_blocked }}</strong></article><article class="metric"><span>Cobertura fiscal</span><strong>{{ "%.1f"|format(detail.fiscal_completion_percent)|replace(".", ",") }}%</strong><small>{{ detail.fiscally_complete }} / {{ detail.total_products }}</small></article></section>
<section class="cards"><article class="metric"><span>Produtos</span><strong>{{ detail.total_products }}</strong></article><article class="metric"><span>Completos</span><strong>{{ detail.fiscally_complete }}</strong></article><article class="metric"><span>Bloqueados com segurança</span><strong>{{ detail.safe_blocked }}</strong></article><article class="metric"><span>Falhas técnicas</span><strong>{{ detail.failed_products }}</strong></article><article class="metric"><span>Não classificados</span><strong>{{ detail.unclassified_products }}</strong></article><article class="metric"><span>Cobertura fiscal</span><strong>{{ "%.1f"|format(detail.fiscal_completion_percent)|replace(".", ",") }}%</strong><small>{{ detail.fiscally_complete }} / {{ detail.total_products }} completos</small></article></section>
{% if detail.coverage_invariant_pass %}<p class="coverage-status">Cobertura reconciliada para este servidor.</p>{% else %}<p class="coverage-status error">Inconsistência de cobertura: {{ detail.coverage_overcount_products }} acima do total{% if detail.coverage_invalid_counts %}; existem contagens negativas{% endif %}.</p>{% endif %}
<nav class="tabs" aria-label="Seções da instalação">{% for name,label in [("overview","Resumo"),("fiscal","Fiscal"),("blockers","Blockers"),("jobs","Jobs"),("events","Eventos")] %}<a class="{% if tab==name %}active{% endif %}" href="?tab={{ name }}">{{ label }}</a>{% endfor %}</nav>
<section class="split"><article class="panel"><div class="panel-title"><h2>Contexto da empresa</h2><a class="secondary" href="/agents/{{ detail.agent_id }}/reprocess-confirmation">Reprocessar tudo</a></div><dl class="facts"><dt>Cliente</dt><dd>{{ detail.client or "-" }}</dd><dt>Nome de exibição</dt><dd>{{ detail.display_name or "-" }}</dd><dt>Hostname</dt><dd>{{ detail.hostname or "-" }}</dd><dt>Empresa</dt><dd>{{ detail.company_name or detail.company_code or "-" }}</dd><dt>UF</dt><dd>{{ detail.company_uf or "-" }}</dd><dt>CRT</dt><dd>{{ detail.company_crt or "-" }}</dd><dt>Versão do agent</dt><dd>{{ detail.agent_version or "Desconhecida" }} · {{ detail.version_drift|title }}</dd><dt>Último heartbeat</dt><dd title="{{ detail.last_seen_at }}">{{ detail.last_seen_display }} · {{ detail.last_seen_age }}</dd><dt>Firebird</dt><dd>{{ detail.firebird_version or "-" }} · ODS {{ detail.firebird_ods or "-" }}</dd><dt>IDs técnicos</dt><dd>Agent {{ detail.technical_ids.agent_id }} · instalação {{ detail.technical_ids.installation_db_id }}</dd></dl></article>
<section class="split"><article class="panel"><div class="panel-title"><h2>Contexto da empresa</h2><a class="secondary" href="/agents/{{ detail.agent_id }}/reprocess-confirmation">Reprocessar tudo</a></div><dl class="facts"><dt>Cliente</dt><dd>{{ detail.client or "Não informado" }}</dd><dt>Nome de exibição</dt><dd>{{ detail.display_name or "Não informado" }}</dd><dt>Hostname</dt><dd>{{ detail.hostname or "Não informado" }}</dd><dt>Empresa</dt><dd>{{ detail.company_name or detail.company_code or "Não informado" }}</dd><dt>UF</dt><dd>{{ detail.company_uf or "Não informado" }}</dd><dt>CRT</dt><dd>{{ detail.company_crt or "Não informado" }}</dd><dt>Versão do agent</dt><dd>{{ detail.agent_version_display }}</dd><dt>Último heartbeat</dt><dd title="{{ detail.last_seen_at }}">{{ detail.last_seen_display }} · {{ detail.last_seen_age }}</dd><dt>Firebird</dt><dd>{{ detail.firebird_version or "Não informado" }} · ODS {{ detail.firebird_ods or "Não informado" }}</dd><dt>IDs técnicos</dt><dd>Agent {{ detail.technical_ids.agent_id }} · instalação {{ detail.technical_ids.installation_db_id }}</dd></dl></article>
<article class="panel"><div class="panel-title"><h2>Eventos recentes</h2><a href="#eventos">Ver eventos</a></div>{% for event in events[:8] %}<div class="event-row"><span class="event-time">{{ event.occurred_display }}</span><span><b>{{ event.event_type }}</b><small>{{ event.message }}{% if event.job_id %} · <a href="/jobs?job_id={{ event.job_id }}">Job {{ event.job_id }}</a>{% endif %}{% if event.product_code %} · <a href="/blockers?agent_id={{ detail.agent_id }}&amp;product_code={{ event.product_code|urlencode }}">Produto {{ event.product_code }}</a>{% endif %}</small></span><em class="severity {{ event.severity|lower }}">Severidade: {{ event.severity }}</em></div>{% else %}<p class="empty-state">Nenhum evento.</p>{% endfor %}</article></section>
<section class="panel" id="eventos"><div class="panel-title"><div><h2>Eventos operacionais</h2><p class="muted">Filtros aplicados no servidor.</p></div></div><form class="filters compact-filters" method="get"><input type="hidden" name="tab" value="events"><label>Severidade<select name="severity"><option value="">Todas</option>{% for value in ["INFO","WARNING","ERROR","CRITICAL"] %}<option value="{{ value }}" {% if severity==value %}selected{% endif %}>{{ value }}</option>{% endfor %}</select></label><label>Tipo de evento<input name="event_type" value="{{ event_type }}"></label><label>Job ID<input name="job_id" value="{{ job_id }}" inputmode="numeric"></label><label>Produto<input name="product_code" value="{{ product_code }}"></label><label>Desde<input type="date" name="since" value="{{ since }}"></label><label>Até<input type="date" name="until" value="{{ until }}"></label><button class="secondary" type="submit">Filtrar eventos</button></form><nav class="pagination" aria-label="Paginação de eventos">{% if event_page > 1 %}<a href="/agents/{{ detail.agent_id }}?event_page={{ event_page - 1 }}{% if event_query_base %}&{{ event_query_base }}{% endif %}">Anterior</a>{% endif %}<span>Página {{ event_page }}</span>{% if has_more %}<a href="/agents/{{ detail.agent_id }}?event_page={{ event_page + 1 }}{% if event_query_base %}&{{ event_query_base }}{% endif %}">Próxima</a>{% endif %}</nav></section>
<section class="panel"><div class="panel-title"><h2>Blockers abertos</h2><a href="/blockers?agent_id={{ detail.agent_id }}">Ver todos</a></div><div class="bar-grid">{% for key in ["CFOP","ST","ICMS","CSOSN","NCM","OTHER"] %}<div class="bar-row"><span>{{ key }}</span><b>{{ blocker_breakdown[key] }}</b></div>{% endfor %}</div>{% for item in blockers["items"][:8] %}<a class="list-row" href="{{ item.link }}"><span><b>{{ item.product_code }}</b> · {{ item.field }}<small>{{ item.reason }} · {{ item.age }}</small></span><em class="badge blocked">{{ item.blocker_type }}</em></a>{% else %}<p class="empty-state">Nenhum blocker aberto.</p>{% endfor %}</section>
+1 -1
View File
@@ -2,6 +2,6 @@
{% block title %}Agents | GTI Fiscal{% endblock %}
{% block content %}
<div class="page-heading"><div><p class="eyebrow">INVENTÁRIO OPERACIONAL</p><h1>Agents / Servidores</h1><p class="muted">Identidade do cliente primeiro; IDs técnicos ficam no detalhe.</p></div><form class="filters" method="get"><label>Cliente, instalação ou hostname<input name="search" value="{{ search }}"></label><label>Saúde técnica<select name="health"><option value="">Todos</option>{% for value in ["ONLINE","DEGRADED","OFFLINE","FAILED","UNKNOWN"] %}<option value="{{ value }}" {% if health_filter==value %}selected{% endif %}>{{ value|title }}</option>{% endfor %}</select></label><label>Ordenação<select name="sort"><option value="display_name" {% if sort=="display_name" %}selected{% endif %}>Nome de exibição</option><option value="worst_coverage" {% if sort=="worst_coverage" %}selected{% endif %}>Pior cobertura fiscal</option></select></label><button class="secondary" type="submit">Filtrar</button></form></div>
<div class="panel table-wrap"><table><caption>Instalações monitoradas</caption><thead><tr><th>Cliente</th><th>Nome de exibição</th><th>Hostname</th><th>Saúde técnica</th><th>Status fiscal</th><th>Cobertura fiscal</th><th>Produtos</th><th>Versão</th><th>Último heartbeat</th></tr></thead><tbody>{% for item in result["items"] %}<tr><td><a href="/agents/{{ item.agent_id }}"><b>{{ item.client or "Cliente não identificado" }}</b></a></td><td>{{ item.display_name or "-" }}<small>ID técnico: {{ item.installation_id }}</small></td><td>{{ item.hostname or "-" }}</td><td><span class="badge {{ item.technical_health|lower }}">Saúde técnica: {{ item.technical_health_label }}</span></td><td><span class="badge fiscal-{{ item.fiscal_status|lower }}">Status fiscal: {{ item.fiscal_status_label }}</span>{% if item.safe_blocked %}<small>{{ item.safe_blocked }} bloqueados com segurança</small>{% endif %}</td><td><strong>{{ "%.1f"|format(item.fiscal_completion_percent)|replace(".", ",") }}%</strong><small>{{ item.fiscally_complete }} / {{ item.total_products }}</small></td><td>{{ item.total_products }}<small>{{ item.failed_products }} falhas</small></td><td>{{ item.agent_version or "Desconhecida" }}<small>{{ item.version_drift|title }}</small></td><td title="{{ item.last_seen_at }}">{{ item.last_seen_display }}<small>{{ item.last_seen_age }}</small></td></tr>{% else %}<tr><td colspan="9" class="empty-state">Nenhuma instalação encontrada.</td></tr>{% endfor %}</tbody></table></div>
<div class="panel table-wrap"><table><caption>Instalações monitoradas</caption><thead><tr><th>Cliente</th><th>Nome de exibição</th><th>Hostname</th><th>Saúde técnica</th><th>Status fiscal</th><th>Cobertura fiscal</th><th>Produtos</th><th>Versão</th><th>Último heartbeat</th></tr></thead><tbody>{% for item in result["items"] %}<tr><td><a href="/agents/{{ item.agent_id }}"><b>{{ item.client or "Cliente não identificado" }}</b></a></td><td>{{ item.display_name or "Não informado" }}</td><td>{{ item.hostname or "Não informado" }}</td><td><span class="badge {{ item.technical_health|lower }}">Saúde técnica: {{ item.technical_health_label }}</span></td><td><span class="badge fiscal-{{ item.fiscal_status|lower }}">Status fiscal: {{ item.fiscal_status_label }}</span><small>{{ item.fiscally_complete }} completos · {{ item.safe_blocked }} bloqueados com segurança · {{ item.failed_products }} falhas · {{ item.unclassified_products }} não classificados</small>{% if not item.coverage_invariant_pass %}<small class="error">Inconsistência: {{ item.coverage_overcount_products }} acima do total{% if item.coverage_invalid_counts %}; contagens negativas{% endif %}</small>{% endif %}</td><td><strong>{{ "%.1f"|format(item.fiscal_completion_percent)|replace(".", ",") }}%</strong><small>{{ item.fiscally_complete }} / {{ item.total_products }} completos</small></td><td>{{ item.total_products }}<small>{{ item.failed_products }} falhas técnicas</small></td><td>{{ item.agent_version_display }}</td><td title="{{ item.last_seen_at }}">{{ item.last_seen_display }}<small>{{ item.last_seen_age }}</small></td></tr>{% else %}<tr><td colspan="9" class="empty-state">Nenhuma instalação encontrada.</td></tr>{% endfor %}</tbody></table></div>
<nav class="pagination" aria-label="Paginação de agents">{% if result.page > 1 %}<a href="/agents?page={{ result.page - 1 }}{% if query_base %}&{{ query_base }}{% endif %}">Anterior</a>{% endif %}<span>Página {{ result.page }} · {{ result.total }} instalações</span>{% if result.page * result.page_size < result.total %}<a href="/agents?page={{ result.page + 1 }}{% if query_base %}&{{ query_base }}{% endif %}">Próxima</a>{% endif %}</nav>
{% endblock %}
+1 -1
View File
@@ -6,7 +6,7 @@
{% for key,label in [("agents_total","Agents total"),("online","Online"),("degraded","Degraded"),("offline","Offline"),("failed","Failed"),("unknown","Unknown")] %}<article class="metric"><span>{{ label }}</span><strong>{{ summary[key] }}</strong></article>{% endfor %}
</section>
<section class="split">
<article class="panel"><div class="panel-title"><h2>Métricas fiscais</h2><a href="/agents">Ver instalações</a></div><div class="coverage"><div><strong>{{ summary.safe_blocked_products }}</strong><span>Produtos bloqueados</span></div><div><strong>{{ summary.open_blockers }}</strong><span>Blockers abertos</span></div><div><strong>{{ summary.total_products }}</strong><span>Produtos avaliados</span></div><div><strong>{{ summary.fiscally_complete }}</strong><span>Completos</span></div></div></article>
<article class="panel"><div class="panel-title"><h2>Cobertura dos produtos</h2><a href="/agents">Ver instalações</a></div><div class="coverage">{% for key,label in [("total_products","Produtos avaliados"),("fiscally_complete","Completos"),("safe_blocked","Bloqueados com segurança"),("failed_products","Falhas técnicas"),("unclassified_products","Não classificados") ] %}<div><strong>{{ summary[key] }}</strong><span>{{ label }}</span></div>{% endfor %}</div>{% if summary.coverage_invariant_pass %}<p class="coverage-status">Cobertura reconciliada: total = completos + bloqueados com segurança + falhas técnicas + não classificados.</p>{% else %}<p class="coverage-status error">Inconsistência de cobertura: {{ summary.coverage_overcount_products }} acima do total; {{ summary.coverage_invalid_snapshots }} snapshot(s) com contagens negativas.</p>{% endif %}<div class="registry-summary"><a href="/blockers">Produtos com blockers abertos: <strong>{{ summary.safe_blocked_products }}</strong></a><span>Blockers abertos: <strong>{{ summary.open_blockers }}</strong></span></div></article>
<article class="panel"><div class="panel-title"><h2>Breakdown dos blockers</h2><a href="/blockers">Abrir fila</a></div>{% for key in ["CFOP","ST","ICMS","CSOSN","NCM","OTHER"] %}<div class="bar-row"><span>{{ key }}</span><b>{{ summary.open_blocker_breakdown[key] }}</b></div>{% endfor %}</article>
</section>
<section class="panel attention"><div class="panel-title"><div><p class="eyebrow">FILA DE AÇÃO</p><h2>ATENÇÃO NECESSÁRIA</h2></div><a href="/blockers">Ver todos</a></div>{% for item in summary.attention_items[:10] %}<a class="list-row" href="{{ item.link }}"><span><b>{{ item.client or "Cliente não identificado" }}</b> · {{ item.display_name or "Instalação" }}<small>{{ item.hostname or "hostname indisponível" }} · {{ item.problem }}</small></span><em class="badge {{ item.technical_health|lower }}">{{ item.age }}</em></a>{% else %}<p class="empty-state">Nenhum item requer atenção imediata.</p>{% endfor %}</section>
+1 -1
View File
@@ -1 +1 @@
<!doctype html><html lang="pt-BR"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Login | GTI Fiscal</title><link rel="stylesheet" href="/static/control-panel.css"></head><body class="login-page"><form class="login-card" method="post" action="/login"><p class="eyebrow">GTI FISCAL / CONTROL PANEL</p><h1>Entrar no painel</h1>{% if error %}<div class="alert error">{{ error }}</div>{% endif %}<label>Usuário<input name="username" autocomplete="username" required></label><label>Senha<input name="password" type="password" autocomplete="current-password" required></label><button class="primary">Acessar</button></form></body></html>
<!doctype html><html lang="pt-BR"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Login | GTI Fiscal</title><link rel="stylesheet" href="/static/control-panel.css"></head><body class="login-page"><form class="login-card" method="post" action="/login"><p class="eyebrow">GTI FISCAL / CONTROL PANEL</p><h1>Entrar no painel</h1>{% if error %}<div class="alert error">{{ error }}</div>{% endif %}<label for="login-username">Usuário</label><input id="login-username" name="username" autocomplete="username" required><label for="login-password">Senha</label><input id="login-password" name="password" type="password" autocomplete="current-password" required><button class="primary" type="submit">Acessar</button></form></body></html>
@@ -17,6 +17,8 @@ def test_fixture_creates_one_hundred_varied_installations_and_events():
assert {row.agent_health for row in snapshots} == {"ONLINE", "DEGRADED", "OFFLINE", "FAILED", "UNKNOWN"}
assert {row.version_drift for row in snapshots} == {"CURRENT", "OUTDATED", "UNKNOWN"}
assert {row.installation_id for row in session.query(Installation).all()} == {str(uuid.uuid5(uuid.NAMESPACE_DNS, f"fixture-{index}")) for index in range(100)}
assert all(row.fiscally_complete + row.safe_blocked + row.failed_products == row.total_products for row in snapshots)
assert all(row.coverage_percent == round(row.fiscally_complete * 100 / row.total_products, 1) for row in snapshots)
blockers = session.query(BlockedProduct).all()
assert len({row.blocker_fingerprint for row in blockers}) == len(blockers)
+86 -1
View File
@@ -22,6 +22,8 @@ def _panel_client():
snapshot = session.scalar(select(AgentSnapshot).where(AgentSnapshot.installation_id == installation.id))
snapshot.total_products = 50
snapshot.fiscally_complete = 49
snapshot.safe_blocked = 1
snapshot.failed_products = 0
snapshot.coverage_percent = 98
job = Job(
agent_id=agent_id,
@@ -80,7 +82,7 @@ def test_dashboard_renders_operational_queue_labels_and_human_dates():
assert response.status_code == 200
for label in ("Agents total", "Online", "Degraded", "Offline", "Failed", "Unknown"):
assert label in response.text
assert "Produtos bloqueados" in response.text
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
@@ -89,6 +91,30 @@ def test_dashboard_renders_operational_queue_labels_and_human_dates():
assert not re.search(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}", response.text)
def test_dashboard_coverage_displays_reconciled_product_buckets():
client, _, _ = _panel_client()
response = client.get("/")
assert response.status_code == 200
for label in ("Produtos avaliados", "Completos", "Bloqueados com segurança", "Falhas técnicas", "Não classificados"):
assert label in response.text
assert "Produtos com blockers abertos" in response.text
assert "Cobertura reconciliada" in response.text
def test_login_has_centered_card_and_explicit_field_labels():
client, _, _ = _panel_client()
response = client.get("/login")
assert response.status_code == 200
assert 'class="login-page"' in response.text
assert 'class="login-card"' in response.text
assert '<label for="login-username">Usuário</label>' in response.text
assert '<label for="login-password">Senha</label>' in response.text
def test_dashboard_lists_bounded_failed_job_rows_with_identity_error_age_and_empty_state():
client, agent_id, _ = _panel_client()
factory = client._session_factory
@@ -159,6 +185,27 @@ def test_agents_render_separate_health_and_fiscal_status_with_server_sort():
assert 'value="worst_coverage"' in response.text
def test_agent_list_hides_technical_uuid_and_explains_remainder():
client, agent_id, _ = _panel_client()
with client._session_factory() as session:
installation = session.scalar(select(Installation).where(Installation.agent_id == agent_id))
technical_uuid = installation.installation_id
agent = session.get(Agent, agent_id)
agent.agent_version = None
snapshot = session.scalar(select(AgentSnapshot).where(AgentSnapshot.installation_id == installation.id))
snapshot.version_drift = "UNKNOWN"
session.commit()
response = client.get("/agents")
assert response.status_code == 200
assert technical_uuid not in response.text
assert "classificados" in response.text
assert "bloqueados com segurança" in response.text
assert "falhas técnicas" in response.text
assert "<td>Desconhecida<small>Desconhecida</small></td>" not in response.text
def test_agent_list_and_detail_show_localized_last_seen_with_iso_tooltip():
client, agent_id, _ = _panel_client()
expected_iso = '2026-09-22T19:11:37+00:00'
@@ -310,6 +357,44 @@ def test_agent_detail_resolves_agent_after_first_hundred_installations():
assert "Late ERP 104" in response.text
def test_agent_detail_reconciles_coverage_and_formats_missing_technical_fields():
client, agent_id, _ = _panel_client()
with client._session_factory() as session:
agent = session.get(Agent, agent_id)
agent.agent_version = None
installation = session.scalar(select(Installation).where(Installation.agent_id == agent_id))
snapshot = session.scalar(select(AgentSnapshot).where(AgentSnapshot.installation_id == installation.id))
snapshot.version_drift = "UNKNOWN"
session.commit()
response = client.get(f"/agents/{agent_id}")
assert response.status_code == 200
for label in ("Produtos", "Completos", "Bloqueados com segurança", "Falhas técnicas", "Não classificados", "Cobertura fiscal"):
assert label in response.text
assert "Não informado" in response.text
assert "Desconhecida" in response.text
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 not re.search(r"<(?:a|button)\b[^>]*>\s*</(?:a|button)>", context)
def test_agent_detail_reports_negative_coverage_counts_as_data_inconsistency():
client, agent_id, _ = _panel_client()
with client._session_factory() as session:
installation = session.scalar(select(Installation).where(Installation.agent_id == agent_id))
snapshot = session.scalar(select(AgentSnapshot).where(AgentSnapshot.installation_id == installation.id))
snapshot.failed_products = -1
session.commit()
response = client.get(f"/agents/{agent_id}")
assert response.status_code == 200
assert "contagens negativas" in response.text
def test_agent_event_filters_lookahead_and_safe_event_reference_links_and_confirmation():
client, agent_id, _ = _panel_client()
factory = client._session_factory
@@ -100,6 +100,70 @@ def test_dashboard_reconciles_five_health_states_and_counts_distinct_safe_produc
assert summary["online"] == 1 and summary["safe_blocked"] == 2
def test_snapshot_coverage_exposes_unclassified_residual_and_overcount():
session_factory = setup_db()
with session_factory() as session:
tenant, agent, installation, snapshot = _installation(session, 1, coverage=80, blocked=1)
snapshot.total_products = 100
snapshot.fiscally_complete = 96
snapshot.safe_blocked = 1
snapshot.failed_products = 1
session.commit()
item = list_installations(session, page_size=5)["items"][0]
assert item["unclassified_products"] == 2
assert item["coverage_overcount_products"] == 0
assert item["coverage_invariant_pass"] is True
assert item["total_products"] == item["fiscally_complete"] + item["safe_blocked"] + item["failed_products"] + item["unclassified_products"]
snapshot.fiscally_complete = 99
session.commit()
inconsistent = list_installations(session, page_size=5)["items"][0]
assert inconsistent["unclassified_products"] == 0
assert inconsistent["coverage_overcount_products"] == 1
assert inconsistent["coverage_invariant_pass"] is False
def test_dashboard_coverage_totals_reconcile_and_keep_registry_metric_separate():
session_factory = setup_db()
with session_factory() as session:
tenant, _, installation, snapshot = _installation(session, 1, coverage=96, blocked=1)
snapshot.total_products = 100
snapshot.fiscally_complete = 96
snapshot.safe_blocked = 1
snapshot.failed_products = 1
_blocker(session, tenant, product="P-OPEN", field="CFOP", index=91, installation_id=installation.id)
session.commit()
summary = dashboard_summary(session, now=NOW)
assert summary["total_products"] == 100
assert summary["fiscally_complete"] == 96
assert summary["safe_blocked"] == 1
assert summary["failed_products"] == 1
assert summary["unclassified_products"] == 2
assert summary["coverage_overcount_products"] == 0
assert summary["coverage_invariant_pass"] is True
assert summary["safe_blocked_products"] == 1
assert summary["total_products"] == summary["fiscally_complete"] + summary["safe_blocked"] + summary["failed_products"] + summary["unclassified_products"]
def test_dashboard_marks_inconsistent_snapshot_partition_instead_of_clamping():
session_factory = setup_db()
with session_factory() as session:
_, _, _, snapshot = _installation(session, 1, coverage=100, blocked=2)
snapshot.total_products = 10
snapshot.fiscally_complete = 9
snapshot.safe_blocked = 2
snapshot.failed_products = 0
session.commit()
summary = dashboard_summary(session, now=NOW)
assert summary["coverage_overcount_products"] == 1
assert summary["coverage_invariant_pass"] is False
def test_attention_items_are_ranked_and_capped_at_ten():
session_factory = setup_db()
with session_factory() as session:
+20 -3
View File
@@ -21,15 +21,32 @@ Technical health describes Agent/installation availability and processing
failures. Fiscal status and fiscal completion are separate: safe-blocked fiscal
work does not make an otherwise healthy Agent technically degraded or failed.
Agent views show product total, fiscally complete count, safe-blocked count,
failed count, and fiscal completion percentage. The percentage is
technical-failure count, unclassified residual, and fiscal completion percentage.
At installation/snapshot grain, coverage is reconciled as:
```text
TOTAL_PRODUCTS = COMPLETE + SAFE_BLOCKED + FAILED_PRODUCTS + UNCLASSIFIED
```
`UNCLASSIFIED` is the positive residual `TOTAL_PRODUCTS - COMPLETE -
SAFE_BLOCKED - FAILED_PRODUCTS`: the snapshot did not assign those products to
one of its explicit states. It is an observability bucket, not a newly inferred
fiscal state. If explicit category counts exceed the total or contain negative
values, the panel shows an inconsistency instead of clamping it away. Fixture
snapshots assign all products explicitly and therefore reconcile to zero
unclassified products. The fiscal completion percentage is
`fiscally_complete / total_products * 100`, rounded to one decimal place and
bounded to 0–100; when the total is zero, the stored coverage percentage is
used. The fiscal status badge is `BLOCKED` when safe-blocked work exists,
`COMPLETE` when all products are complete, and otherwise `INCOMPLETE`.
Two blocker counts must remain distinct:
Coverage-state counts and blocker-registry counts have different sources and
must remain distinct:
- **Safe-blocked products**: distinct open `(tenant_id, product_code)` pairs.
- **Safe-blocked coverage products**: sum of the Agent snapshot safe-blocked
count at installation grain; this participates in the coverage invariant.
- **Products with open blockers**: distinct open `(tenant_id, product_code)`
pairs in the blocker registry.
- **Open blockers**: open blocker rows; one product can have multiple rows for
different fields or evidence.
@@ -0,0 +1,81 @@
# Control Panel Final Pre-Deploy Fixes V1 Implementation Plan
> **For agentic workers:** Execute inline; the operator mission authorizes autonomous completion. Steps use checkbox syntax for tracking.
**Goal:** Reconcile fiscal coverage counts, correct the fixture gap, and apply the narrowly scoped login/Agent-list/Agent-detail presentation fixes before deploy freeze.
**Architecture:** Preserve the FastAPI/Jinja2/CSS/vanilla-JS panel and fiscal/Agent behavior. Add a derived, explicitly named `unclassified_products` residual from snapshot coverage, expose a consistency flag for over-counted categories, correct only fixture counts that are known synthetic gaps, and make requested UI-only changes.
**Tech Stack:** Python 3.12, FastAPI, Jinja2, SQLAlchemy, CSS, pytest, Docker Compose local runtime.
## Global Constraints
- Do not deploy, alter fiscal engine/requiredness, call AI, enable Firebird writes, change architecture, add SPA, modify Proxmox, or integrate GTI Gerenciador.
- A product coverage state is counted at installation/snapshot grain; existing distinct blocker registry totals remain separately labeled.
- Define `UNCLASSIFIED = TOTAL - COMPLETE - SAFE_BLOCKED - FAILED_PRODUCTS` only when nonnegative; show an inconsistency warning and fail `coverage_invariant_pass` when the explicit categories exceed total.
- Do not use an unexplained generic “other” label; the residual is “Não classificados (sem estado informado)” and documentation says precisely that snapshot telemetry did not assign a known state.
- Keep Agent percentage and technical IDs in detail/API; remove UUID only from the main Agents table.
- Do not modify Firebird databases or run fixture generation against production/client data.
---
### Task 1: Prove coverage gap and add view-model/UI tests
**Files:**
- Modify `central/tests/test_operational_view_models.py`
- Modify `central/tests/test_control_panel_fixture.py`
- Modify `central/tests/test_control_panel_pages.py`
- Add focused assertions to `central/tests/test_control_plane.py` only if needed.
- [ ] Test coverage partition arithmetic for normal, residual, and over-counted snapshots.
- [ ] Test dashboard sums total/complete/snapshot-safe-blocked/failed and explicit residual; require nonnegative invariant pass only when components do not exceed total.
- [ ] Test Agent detail/list expose explicit residual and the fiscal percentage.
- [ ] Test fixture total equals complete + safe-blocked + failed + residual; assert fixture residual zero after correction.
- [ ] Test login form labels/container, Agent list does not contain technical UUID, detail shows missing fields as `Não informado`/`Desconhecida`, and no empty green anchor/button exists in company card.
- [ ] Run targeted tests and capture expected failures before implementation.
### Task 2: Implement reconciled coverage view-models and fixture
**Files:**
- Modify `central/app/control_plane.py`
- Modify `central/scripts/generate_control_panel_fixture.py`
- Modify dashboard, Agent list, and Agent detail templates.
**Interfaces:**
- Add a single helper returning `complete`, `safe_blocked`, `failed`, `unclassified`, `total`, `consistent`, and `overcount` for a snapshot.
- Agent view adds `unclassified_products`, `coverage_invariant_pass`, `coverage_overcount_products`.
- Dashboard returns `safe_blocked_coverage_products`, `unclassified_products`, `coverage_invariant_pass`, and `coverage_overcount_products`; retain registry `safe_blocked_products` as a separately labeled blocker metric if displayed.
- [ ] Implement arithmetic with explicit consistency behavior; do not clamp or silently hide over-count.
- [ ] Dashboard SQL aggregate sums snapshot fields and computes residual at presentation/query aggregation without loading products.
- [ ] Fixture sets complete to `total - safe_blocked - failed_products`; set coverage percentage from complete/total rounded one decimal.
- [ ] Add template rows/cards for safe-blocked coverage, failed technical products, and unclassified products; display inconsistency warning when needed.
- [ ] Agent list explains the remaining percentage beside complete/blocked/failed/unclassified counts.
- [ ] Run targeted model/fixture/page tests green.
### Task 3: Minimal visual fixes and technical missing values
**Files:**
- Modify `central/templates/login.html`
- Modify `central/templates/agents.html`
- Modify `central/templates/agent_detail.html`
- Modify `central/static/control-panel.css`
- Modify `central/tests/test_control_panel_pages.py`
- [ ] Add a login wrapper/card sizing and center layout, explicit `for`/`id` labels, spacing, responsive width, and existing accent colors.
- [ ] Remove the installation UUID secondary text from the Agents table; retain it in detail technical IDs/API.
- [ ] Replace empty/unknown technical displays with `Não informado` and `Desconhecida`; do not invent Firebird version/ODS or version drift.
- [ ] Investigate green empty control: confirm rendered anchor is a real reprocess link with visible text; if visually empty due styles, correct styles; if action absent, remove element. Add assertion for non-empty accessible name/text.
- [ ] Run focused page tests green.
### Task 4: Documentation and final verification
**Files:**
- Modify `docs/CONTROL_PANEL_V1.md` and/or `docs/CONTROL_PANEL_OPERATIONAL_UX_V1.md` only if required to define the residual precisely.
- [ ] Document that unclassified is the snapshot residual without a supplied operational state, not a fiscal rule.
- [ ] Run exactly `python -m pytest tests -q`, `python -m pytest central/tests -q`, and `python -m pytest agent/tests -q`.
- [ ] Rebuild only local Compose `fiscal-hub`; verify PostgreSQL healthy and Hub `/health` 200.
- [ ] Smoke `/login`, `/`, `/agents`, `/agents/{id}`; check coverage reconciliation, login, UUID removal, visible reprocess control, and missing technical values.
- [ ] Measure route latency and record maximum.
- [ ] Verify flags, no deploy/Firebird operations, diff check, commits, and clean worktree; report exact requested final fields.