fix: reconcile final control panel coverage UX
This commit is contained in:
@@ -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"}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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 }}&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>
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 @@
|
||||
<!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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user