feat: operational panel UI - active-scope dashboard, drill-down cards, scoped Agents/Blockers/Jobs, PT-BR labels, responsive CSS

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
gti
2026-09-24 13:38:40 -03:00
co-authored by Claude Sonnet 5
parent bfd4d28aa4
commit 9a142975a6
13 changed files with 725 additions and 88 deletions
+107 -31
View File
@@ -4,19 +4,34 @@ from __future__ import annotations
from datetime import datetime, timedelta, timezone
from decimal import Decimal, ROUND_HALF_UP
from urllib.parse import urlencode
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 .models import Agent, AgentEvent, AgentSnapshot, AuditEvent, BlockedProduct, Installation, Job, Tenant
from .panel_presentation import format_age, format_duration, format_ui_datetime, health_label, operational_priority
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
HEALTH_STATES = ("ONLINE", "DEGRADED", "OFFLINE", "FAILED", "UNKNOWN")
BLOCKER_FIELDS = ("CFOP", "ST", "ICMS", "CSOSN", "NCM", "OTHER")
ATTENTION_CANDIDATE_LIMIT = 10
INACTIVE_ENROLLMENT_STATES = ("REVOKED", "FAILED", "EXPIRED")
def operational_installation_predicate():
"""Only installations eligible for current operational indicators.
Legacy Agents without an enrollment remain eligible. Disabling job
processing is not the same as deactivating an installation.
"""
inactive_enrollment = select(AgentEnrollment.id).where(
AgentEnrollment.agent_id == Agent.id,
AgentEnrollment.status.in_(INACTIVE_ENROLLMENT_STATES),
).exists()
return and_(Tenant.active.is_(True), Agent.enabled.is_(True), ~inactive_enrollment)
def _page(page: int, page_size: int):
@@ -167,7 +182,7 @@ def installation_view(installation, agent, tenant, snapshot=None, *, now=None):
"coverage_percent": snapshot.coverage_percent if has_snapshot else None,
"fiscal_completion_percent": _fiscal_completion(snapshot) if has_snapshot else None,
"fiscal_status": fiscal_status if has_snapshot else "UNKNOWN",
"fiscal_status_label": fiscal_status.title() if has_snapshot else "Não informado",
"fiscal_status_label": fiscal_status_label(fiscal_status) if has_snapshot else "Não informado",
"blockers": snapshot.blockers or {},
"last_job_status": snapshot.last_job_status,
"last_job_at": _iso(snapshot.last_job_at),
@@ -186,7 +201,7 @@ def installation_view(installation, agent, tenant, snapshot=None, *, now=None):
def _installation_query_filters(query, *, client=None, version=None, search=None, last_seen_before=None):
if client:
query = query.where(or_(Tenant.name.ilike(f"%{client}%"), Installation.customer_code.ilike(f"%{client}%")))
if version:
if version and version != "outdated":
query = query.where(Agent.agent_version == version)
if search:
query = query.where(or_(Installation.display_name.ilike(f"%{search}%"), Installation.hostname.ilike(f"%{search}%"), Tenant.name.ilike(f"%{search}%")))
@@ -195,11 +210,17 @@ def _installation_query_filters(query, *, client=None, version=None, search=None
return query
def list_installations(session, *, page=1, page_size=50, client=None, health=None, version=None, search=None, last_seen_before=None, sort="display_name", now=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)
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)
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())
elif scope != "all":
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)))
health_expression = _health_expression(now)
if health:
query = query.where(health_expression == health)
@@ -208,7 +229,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) for i, a, t, s in rows]
items = [{**installation_view(i, a, t, s, now=now), "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}
@@ -223,8 +244,10 @@ def _blocker_item(row, tenant, installation=None, agent=None, *, now=None):
"description": row.description,
"field": row.field,
"blocker_type": row.blocker_type,
"blocker_type_label": blocker_type_label(row.blocker_type),
"reason": row.reason_code,
"reason_code": row.reason_code,
"reason_label": blocker_reason_label(row.reason_code),
"evidence_summary": row.evidence_summary,
"first_seen_at": _iso(row.first_seen_at),
"last_seen_at": _iso(row.last_seen_at),
@@ -235,6 +258,7 @@ def _blocker_item(row, tenant, installation=None, agent=None, *, now=None):
"resolved_at": _iso(row.resolved_at),
"resolution_source": row.resolution_source,
"priority": operational_priority(row),
"priority_label": priority_label(operational_priority(row)),
"installation_id": installation.installation_id if installation else None,
"installation_db_id": installation.id if installation else None,
"display_name": installation.display_name if installation else None,
@@ -260,8 +284,12 @@ def _parse_age(value):
return None
def _blockers_query(session, *, tenant_id=None, agent_id=None, product_code=None, blocker_type=None, field=None, status="OPEN", client=None, installation=None, search=None, age=None):
def _blockers_query(session, *, tenant_id=None, agent_id=None, product_code=None, blocker_type=None, field=None, reason_code=None, status="OPEN", client=None, installation=None, search=None, age=None, scope="active"):
query = select(BlockedProduct, Tenant, Installation, Agent).join(Tenant, Tenant.id == BlockedProduct.tenant_id).outerjoin(Installation, Installation.id == BlockedProduct.installation_id).outerjoin(Agent, Agent.id == Installation.agent_id)
if scope == "active":
query = query.where(operational_installation_predicate(), Installation.id.is_not(None))
elif scope != "all":
raise ValueError("scope must be active or all")
status = (status or "OPEN").upper()
if status == "OPEN":
query = query.where(BlockedProduct.resolved_at.is_(None))
@@ -276,7 +304,9 @@ def _blockers_query(session, *, tenant_id=None, agent_id=None, product_code=None
if blocker_type:
query = query.where(BlockedProduct.blocker_type == blocker_type)
if field:
query = query.where(BlockedProduct.field == field)
query = query.where(func.upper(BlockedProduct.field) == field.upper())
if reason_code:
query = query.where(BlockedProduct.reason_code == reason_code)
if client:
query = query.where(or_(Tenant.name.ilike(f"%{client}%"), BlockedProduct.company.ilike(f"%{client}%")))
if installation:
@@ -290,8 +320,8 @@ def _blockers_query(session, *, tenant_id=None, agent_id=None, product_code=None
return query
def blocker_filter_summary(session, *, tenant_id=None, agent_id=None, product_code=None, blocker_type=None, field=None, status="OPEN", client=None, installation=None, age=None, search=None):
query = _blockers_query(session, tenant_id=tenant_id, agent_id=agent_id, product_code=product_code, blocker_type=blocker_type, field=field, status=status, client=client, installation=installation, search=search, age=age)
def blocker_filter_summary(session, *, tenant_id=None, agent_id=None, product_code=None, blocker_type=None, field=None, reason_code=None, status="OPEN", client=None, installation=None, age=None, search=None, scope="active"):
query = _blockers_query(session, tenant_id=tenant_id, agent_id=agent_id, product_code=product_code, blocker_type=blocker_type, field=field, reason_code=reason_code, status=status, client=client, installation=installation, search=search, age=age, scope=scope)
open_count = func.sum(case((BlockedProduct.resolved_at.is_(None), 1), else_=0))
open_count, = session.execute(query.with_only_columns(open_count).order_by(None)).one()
rows = session.execute(_blocker_breakdown_query(query)).all()
@@ -314,11 +344,11 @@ def _blocker_breakdown_query(query):
return query.with_only_columns(BlockedProduct.field, func.count(BlockedProduct.id)).group_by(BlockedProduct.field).order_by(None)
def list_blockers(session, *, tenant_id=None, agent_id=None, product_code=None, page=1, page_size=50, blocker_type=None, field=None, resolved=False, client=None, installation=None, status="OPEN", age=None, search=None):
def list_blockers(session, *, tenant_id=None, agent_id=None, product_code=None, page=1, page_size=50, blocker_type=None, field=None, reason_code=None, resolved=False, client=None, installation=None, status="OPEN", age=None, search=None, scope="active"):
page, page_size = _page(page, page_size)
if resolved and status == "OPEN":
status = "RESOLVED"
query = _blockers_query(session, tenant_id=tenant_id, agent_id=agent_id, product_code=product_code, blocker_type=blocker_type, field=field, status=status, client=client, installation=installation, search=search, age=age)
query = _blockers_query(session, tenant_id=tenant_id, agent_id=agent_id, product_code=product_code, blocker_type=blocker_type, field=field, reason_code=reason_code, status=status, client=client, installation=installation, search=search, age=age, scope=scope)
ordered = query.order_by(BlockedProduct.last_seen_at.desc(), BlockedProduct.id.desc())
rows = session.execute(ordered.offset((page - 1) * page_size).limit(page_size)).all()
items = [_blocker_item(blocker, tenant, installation_row, agent, now=datetime.now(timezone.utc)) for blocker, tenant, installation_row, agent in rows]
@@ -335,7 +365,9 @@ def _job_item(row, agent, installation, tenant, *, now=None):
"display_name": installation.display_name if installation else None,
"hostname": installation.hostname if installation else None,
"kind": row.kind,
"kind_label": job_kind_label(row.kind),
"status": row.status,
"status_label": job_status_label(row.status),
"created_at": _iso(row.created_at),
"started_at": _iso(row.started_at),
"finished_at": _iso(row.finished_at),
@@ -349,9 +381,13 @@ def _job_item(row, agent, installation, tenant, *, now=None):
}
def list_jobs(session, *, agent_id=None, job_id=None, page=1, page_size=50, status=None, kind=None, since=None, until=None, client=None, search=None):
def list_jobs(session, *, agent_id=None, job_id=None, page=1, page_size=50, status=None, kind=None, since=None, until=None, client=None, search=None, scope="active"):
page, page_size = _page(page, page_size)
query = select(Job, Agent, Installation, Tenant).join(Agent, Agent.id == Job.agent_id).join(Installation, Installation.agent_id == Agent.id).join(Tenant, Tenant.id == Installation.tenant_id)
if scope == "active":
query = query.where(operational_installation_predicate())
elif scope != "all":
raise ValueError("scope must be active or all")
if agent_id is not None:
query = query.where(Job.agent_id == agent_id)
if job_id is not None:
@@ -376,9 +412,26 @@ def list_jobs(session, *, agent_id=None, job_id=None, page=1, page_size=50, stat
return {"page": page, "page_size": page_size, "items": [_job_item(row, agent, installation, tenant) for row, agent, installation, tenant in rows], "total": total}
def jobs_status_summary(session, *, scope="active"):
"""Bounded job counts for the current operational scope."""
query = (select(Job.status, func.count(Job.id)).join(Agent, Agent.id == Job.agent_id)
.join(Installation, Installation.agent_id == Agent.id)
.join(Tenant, Tenant.id == Installation.tenant_id))
if scope == "active":
query = query.where(operational_installation_predicate())
elif scope != "all":
raise ValueError("scope must be active or all")
counts = {status: 0 for status in ("PENDING", "RUNNING", "COMPLETED", "FAILED")}
for status, count in session.execute(query.group_by(Job.status)):
if status in counts:
counts[status] = int(count)
return counts
def dashboard_summary(session, now=None):
now = _utc_now(now)
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)
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)
health_rows = session.execute(
select(health_expression.label("health"), func.count(Installation.id))
@@ -386,6 +439,7 @@ def dashboard_summary(session, now=None):
.join(Agent, Agent.id == Installation.agent_id)
.join(Tenant, Tenant.id == Installation.tenant_id)
.outerjoin(AgentSnapshot, AgentSnapshot.installation_id == Installation.id)
.where(active)
.group_by(health_expression)
).all()
counts = {state: 0 for state in HEALTH_STATES}
@@ -407,20 +461,23 @@ def dashboard_summary(session, now=None):
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)
).select_from(Installation).join(Agent, Agent.id == Installation.agent_id).join(Tenant, Tenant.id == Installation.tenant_id).outerjoin(AgentSnapshot, AgentSnapshot.installation_id == Installation.id).where(active)
).one()
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
active_blockers = (select(BlockedProduct).join(Installation, Installation.id == BlockedProduct.installation_id)
.join(Agent, Agent.id == Installation.agent_id).join(Tenant, Tenant.id == BlockedProduct.tenant_id)
.where(open_filter, active))
open_blockers = session.scalar(active_blockers.with_only_columns(func.count(BlockedProduct.id)).order_by(None)) or 0
breakdown_expression = func.coalesce(BlockedProduct.field, "OTHER")
breakdown_rows = session.execute(select(breakdown_expression, func.count(BlockedProduct.id)).where(open_filter).group_by(breakdown_expression)).all()
breakdown_rows = session.execute(active_blockers.with_only_columns(breakdown_expression, func.count(BlockedProduct.id)).group_by(breakdown_expression).order_by(None)).all()
open_blocker_breakdown = {field: 0 for field in BLOCKER_FIELDS}
for field, count in breakdown_rows:
key = str(field or "").upper()
open_blocker_breakdown[key if key in open_blocker_breakdown else "OTHER"] += int(count)
product_pairs = select(BlockedProduct.tenant_id, BlockedProduct.product_code).where(open_filter).distinct().subquery()
product_pairs = active_blockers.with_only_columns(BlockedProduct.tenant_id, BlockedProduct.product_code).distinct().subquery()
safe_blocked_products = session.scalar(select(func.count()).select_from(product_pairs)) or 0
blocker_counts = {field: count for field, count in open_blocker_breakdown.items() if count}
@@ -433,28 +490,47 @@ def dashboard_summary(session, now=None):
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()]
blocker_query = select(BlockedProduct, Tenant, Installation, Agent, AgentSnapshot).join(Tenant, Tenant.id == BlockedProduct.tenant_id).outerjoin(Installation, Installation.id == BlockedProduct.installation_id).outerjoin(Agent, Agent.id == Installation.agent_id).outerjoin(AgentSnapshot, AgentSnapshot.installation_id == Installation.id).where(open_filter, func.upper(BlockedProduct.field).in_(BLOCKER_FIELDS[:-1])).order_by(BlockedProduct.last_seen_at.desc(), BlockedProduct.id.desc()).limit(ATTENTION_CANDIDATE_LIMIT)
blocker_candidates = session.execute(blocker_query).all()
group_field = func.upper(func.coalesce(BlockedProduct.field, "OTHER"))
fiscal_priority = case(
(func.upper(BlockedProduct.blocker_type).in_(("TECHNICAL_FAILURE", "CONNECTION_ERROR", "AGENT_OFFLINE", "JOB_FAILED")), 0),
(func.upper(BlockedProduct.blocker_type).in_(("BLOCKED_MISSING_EVIDENCE", "MISSING_EVIDENCE")), 1),
(group_field.in_(BLOCKER_FIELDS[:-1]), 2),
else_=3,
)
blocker_groups = session.execute(
active_blockers.with_only_columns(
Tenant.name, Installation.display_name, Installation.hostname, Agent.id,
group_field, BlockedProduct.blocker_type, BlockedProduct.reason_code,
func.count(BlockedProduct.id), func.count(func.distinct(BlockedProduct.product_code)),
func.max(BlockedProduct.last_seen_at),
).group_by(Tenant.name, Installation.id, Installation.display_name, Installation.hostname,
Agent.id, group_field, BlockedProduct.blocker_type, BlockedProduct.reason_code)
.order_by(fiscal_priority.asc(), func.max(BlockedProduct.last_seen_at).desc(), Agent.id.asc())
.limit(ATTENTION_CANDIDATE_LIMIT)
).all()
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])
for blocker, tenant, installation, agent, snapshot in blocker_candidates:
if installation and agent:
item = installation_view(installation, agent, tenant, snapshot, now=now)
else:
item = {"client": tenant.name, "display_name": None, "hostname": None, "agent_id": None, "technical_health": "UNKNOWN", "health_reason": "tenant-level blocker", "last_seen_at": None, "last_job_at": None}
attention.append((_attention_item(item, f"{blocker.field or blocker.blocker_type}: {blocker.reason_code}", blocker.last_seen_at, rank=4, link=f"/blockers/{blocker.id}", now=now), 4, blocker.id))
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.update(group_count=count, products_affected=products, field=field, priority=priority)
attention.append((entry, rank, agent_id))
for item in outdated_agents:
attention.append((_attention_item(item, "Versão do agent desatualizada", item.get("last_seen_at"), rank=5, now=now), 5, item["agent_id"]))
attention.append((_attention_item(item, "Versão do agent desatualizada", item.get("last_seen_at"), rank=7, now=now), 7, item["agent_id"]))
attention.sort(key=lambda entry: (entry[1], -(entry[0]["timestamp"] is not None), entry[2]))
failed_jobs = session.scalar(select(func.count(Job.id)).where(Job.status == "FAILED")) or 0
active_jobs = select(Job).join(Agent, Agent.id == Job.agent_id).join(Installation, Installation.agent_id == Agent.id).join(Tenant, Tenant.id == Installation.tenant_id).where(active)
failed_jobs = session.scalar(active_jobs.with_only_columns(func.count(Job.id)).where(Job.status == "FAILED").order_by(None)) or 0
failed_job_rows = session.execute(
select(Job, Agent, Installation, Tenant).join(Agent, Agent.id == Job.agent_id)
.join(Installation, Installation.agent_id == Agent.id).join(Tenant, Tenant.id == Installation.tenant_id)
.where(Job.status == "FAILED").order_by(Job.created_at.desc(), Job.id.desc()).limit(10)
.where(Job.status == "FAILED", active).order_by(Job.created_at.desc(), Job.id.desc()).limit(10)
).all()
recent_failed_jobs = []
for job, agent, installation, tenant in failed_job_rows:
@@ -482,7 +558,7 @@ def dashboard_summary(session, now=None):
def blocker_detail(session, blocker_id):
row = session.execute(_blockers_query(session, status="ALL").where(BlockedProduct.id == blocker_id)).first()
row = session.execute(_blockers_query(session, status="ALL", scope="all").where(BlockedProduct.id == blocker_id)).first()
if not row:
return None
blocker, tenant, installation, agent = row
+31
View File
@@ -99,3 +99,34 @@ def health_label(value) -> str:
return "-"
text = str(value)
return _HEALTH_LABELS.get(text.upper(), text)
def _human_label(value, labels):
if not value:
return "Não informado"
text = str(value).upper()
return labels.get(text, str(value).replace("_", " ").lower().capitalize())
def fiscal_status_label(value):
return _human_label(value, {"BLOCKED": "Bloqueado", "COMPLETE": "Completo", "INCOMPLETE": "Incompleto", "UNKNOWN": "Não informado"})
def job_status_label(value):
return _human_label(value, {"PENDING": "Pendente", "RUNNING": "Executando", "COMPLETED": "Concluído", "FAILED": "Falhou"})
def job_kind_label(value):
return _human_label(value, {"SYNC_INCREMENTAL": "Sincronização incremental", "INITIAL_SYNC": "Sincronização inicial", "REPROCESS_ALL": "Reprocessar tudo", "REPROCESS_PRODUCTS": "Reprocessar produtos"})
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"})
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"})
def priority_label(value):
return _human_label(value, {"CRITICAL": "Crítica", "HIGH": "Alta", "NORMAL": "Média", "MEDIUM": "Média", "LOW": "Baixa"})
+20 -10
View File
@@ -14,7 +14,7 @@ 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 .control_plane import blocker_detail, blocker_filter_summary, dashboard_summary, installation_view, list_blockers, list_installations, list_jobs, list_panel_events
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
from .models import AdminUser, Agent, AgentEnrollment, AgentRelease, AgentSnapshot, BlockedProduct, FiscalContextEntry, Installation, Job, Tenant
@@ -162,11 +162,15 @@ def router(session_factory):
finally: session.close()
@routes.get("/agents", response_class=HTMLResponse)
def agents(request: Request, page: int = 1, search: str | None = None, health: str | None = None, sort: str = "display_name"):
def agents(request: Request, page: int = 1, search: str | None = None, health: str | None = None, sort: str = "display_name", scope: str = "active", version: str | None = None):
session, auth_data, redirect = page_auth(request)
if redirect: return redirect
try:
return render(request, "agents.html", {"result": list_installations(session, page=page, page_size=50, search=search, health=health, sort=sort), "auth": auth_data, "search": search or "", "health_filter": health or "", "sort": sort, "query_base": query_base(search=search, health=health, sort=sort)})
scope = scope if scope in ("active", "all") else "active"
health = health if health in ("ONLINE", "DEGRADED", "OFFLINE", "FAILED", "UNKNOWN") else None
sort = sort if sort in ("display_name", "worst_coverage") else "display_name"
version = version if version == "outdated" else None
return render(request, "agents.html", {"result": list_installations(session, page=page, page_size=50, search=search, health=health, sort=sort, scope=scope, version=version), "auth": auth_data, "search": search or "", "health_filter": health or "", "sort": sort, "scope_filter": scope, "version_filter": version or "", "query_base": query_base(search=search, health=health, sort=sort, scope=scope, version=version)})
finally: session.close()
# --- Static /agents/* routes MUST be declared before /agents/{agent_id}:
@@ -278,8 +282,8 @@ def router(session_factory):
events = events[:25]
for event in events:
event["occurred_display"] = format_ui_datetime(datetime.fromisoformat(event["occurred_at"]) if event["occurred_at"] else None)
agent_blockers = list_blockers(session, tenant_id=installation.tenant_id, agent_id=agent_id, page=1, page_size=25)
return render(request, "agent_detail.html", {"detail": detail, "tab": tab, "blockers": agent_blockers, "blocker_breakdown": blocker_filter_summary(session, tenant_id=installation.tenant_id, agent_id=agent_id, status="OPEN")["blocker_breakdown"], "jobs": list_jobs(session, agent_id=agent_id, page=1, page_size=25), "events": events, "event_page": max(1, event_page), "has_more": has_more, "severity": severity or "", "event_type": event_type or "", "job_id": job_id or "", "product_code": product_code or "", "since": since or "", "until": until or "", "event_query_base": query_base(tab=tab, severity=severity, event_type=event_type, job_id=job_id, product_code=product_code, since=since, until=until), "auth": auth_data, "csrf_token": request.cookies.get("gti_csrf", ""), "ops": _ops_view(session, installation, agent)})
agent_blockers = list_blockers(session, tenant_id=installation.tenant_id, agent_id=agent_id, page=1, page_size=25, scope="all")
return render(request, "agent_detail.html", {"detail": detail, "tab": tab, "blockers": agent_blockers, "blocker_breakdown": blocker_filter_summary(session, tenant_id=installation.tenant_id, agent_id=agent_id, status="OPEN", scope="all")["blocker_breakdown"], "jobs": list_jobs(session, agent_id=agent_id, page=1, page_size=25, scope="all"), "events": events, "event_page": max(1, event_page), "has_more": has_more, "severity": severity or "", "event_type": event_type or "", "job_id": job_id or "", "product_code": product_code or "", "since": since or "", "until": until or "", "event_query_base": query_base(tab=tab, severity=severity, event_type=event_type, job_id=job_id, product_code=product_code, since=since, until=until), "auth": auth_data, "csrf_token": request.cookies.get("gti_csrf", ""), "ops": _ops_view(session, installation, agent)})
finally: session.close()
def _ops_view(session, installation, agent):
@@ -463,14 +467,16 @@ def router(session_factory):
finally: session.close()
@routes.get("/blockers", response_class=HTMLResponse)
def blockers(request: Request, page: int = 1, client: str | None = None, installation: str | None = None, field: str | None = None, blocker_type: str | None = None, status: str = "OPEN", age: str | None = None, search: str | None = None, agent_id: int | None = None, product_code: str | None = None):
def blockers(request: Request, page: int = 1, client: str | None = None, installation: str | None = None, field: str | None = None, blocker_type: str | None = None, reason_code: str | None = None, status: str = "OPEN", age: str | None = None, search: str | None = None, agent_id: int | None = None, product_code: str | None = None, scope: str = "active"):
session, auth_data, redirect = page_auth(request)
if redirect: return redirect
try:
filters = {"agent_id": agent_id, "product_code": product_code, "client": client, "installation": installation, "field": field, "blocker_type": blocker_type, "status": status, "age": age, "search": search}
scope = scope if scope in ("active", "all") else "active"
status = status if status in ("OPEN", "RESOLVED", "ALL") else "OPEN"
filters = {"agent_id": agent_id, "product_code": product_code, "client": client, "installation": installation, "field": field, "blocker_type": blocker_type, "reason_code": reason_code, "status": status, "age": age, "search": search, "scope": scope}
result = list_blockers(session, page=page, page_size=50, **filters)
summary = blocker_filter_summary(session, **filters)
return render(request, "blockers.html", {"result": result, "summary": summary, "auth": auth_data, "csrf_token": request.cookies.get("gti_csrf", ""), "client_filter": client or "", "installation_filter": installation or "", "field_filter": field or "", "blocker_type_filter": blocker_type or "", "status_filter": status or "OPEN", "age_filter": age or "", "search": search or "", "query_base": query_base(**filters)})
return render(request, "blockers.html", {"result": result, "summary": summary, "auth": auth_data, "csrf_token": request.cookies.get("gti_csrf", ""), "client_filter": client or "", "installation_filter": installation or "", "field_filter": field or "", "blocker_type_filter": blocker_type or "", "reason_code_filter": reason_code or "", "status_filter": status, "scope_filter": scope, "age_filter": age or "", "search": search or "", "query_base": query_base(**filters), "field_query_base": query_base(**{key: value for key, value in filters.items() if key != "field"})})
finally: session.close()
@routes.get("/blockers/{blocker_id}", response_class=HTMLResponse)
@@ -516,10 +522,14 @@ def router(session_factory):
session.close()
@routes.get("/jobs", response_class=HTMLResponse)
def jobs(request: Request, page: int = 1, status: str | None = None, search: str | None = None, kind: str | None = None, job_id: int | None = None):
def jobs(request: Request, page: int = 1, status: str | None = None, search: str | None = None, kind: str | None = None, job_id: int | None = None, scope: str = "active"):
session, auth_data, redirect = page_auth(request)
if redirect: return redirect
try: return render(request, "jobs.html", {"result": list_jobs(session, page=page, page_size=50, status=status, search=search, kind=kind, job_id=job_id), "auth": auth_data, "status_filter": status or "", "search": search or "", "kind_filter": kind or "", "query_base": query_base(status=status, search=search, kind=kind, job_id=job_id)})
try:
scope = scope if scope in ("active", "all") else "active"
status = status if status in ("PENDING", "RUNNING", "COMPLETED", "FAILED") else None
result = list_jobs(session, page=page, page_size=50, status=status, search=search, kind=kind, job_id=job_id, scope=scope)
return render(request, "jobs.html", {"result": result, "status_counts": jobs_status_summary(session, scope=scope), "auth": auth_data, "status_filter": status or "", "scope_filter": scope, "search": search or "", "kind_filter": kind or "", "job_id_filter": job_id, "query_base": query_base(status=status, search=search, kind=kind, job_id=job_id, scope=scope)})
finally: session.close()
return routes