fix: close task 2 operational review findings
This commit is contained in:
+101
-60
@@ -4,9 +4,9 @@ from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from sqlalchemy import exists, func, or_, select
|
||||
from sqlalchemy.orm import aliased
|
||||
from sqlalchemy import and_, case, 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
|
||||
@@ -15,6 +15,7 @@ from .panel_presentation import format_age, format_duration, format_ui_datetime,
|
||||
|
||||
HEALTH_STATES = ("ONLINE", "DEGRADED", "OFFLINE", "FAILED", "UNKNOWN")
|
||||
BLOCKER_FIELDS = ("CFOP", "ST", "ICMS", "CSOSN", "NCM", "OTHER")
|
||||
ATTENTION_CANDIDATE_LIMIT = 10
|
||||
|
||||
|
||||
def _page(page: int, page_size: int):
|
||||
@@ -25,12 +26,36 @@ def _iso(value):
|
||||
return value.isoformat() if value else None
|
||||
|
||||
|
||||
def _utc_now(value=None):
|
||||
value = value or datetime.now(timezone.utc)
|
||||
return value.replace(tzinfo=timezone.utc) if value.tzinfo is None else value
|
||||
|
||||
|
||||
def _health_expression(now):
|
||||
offline_after = settings.agent_offline_after_seconds or settings.agent_heartbeat_interval_seconds * 3
|
||||
cutoff = _utc_now(now) - timedelta(seconds=offline_after)
|
||||
return case(
|
||||
(Installation.last_seen_at.is_(None), "UNKNOWN"),
|
||||
(or_(AgentSnapshot.agent_health == "FAILED", Installation.last_status == "FAILED"), "FAILED"),
|
||||
(Installation.last_seen_at < cutoff, "OFFLINE"),
|
||||
(or_(AgentSnapshot.failed_products > 0, AgentSnapshot.last_job_status == "FAILED"), "DEGRADED"),
|
||||
else_="ONLINE",
|
||||
)
|
||||
|
||||
|
||||
def _fiscal_completion_expression():
|
||||
total = func.coalesce(AgentSnapshot.total_products, 0)
|
||||
complete = func.coalesce(AgentSnapshot.fiscally_complete, 0)
|
||||
raw = case((total > 0, complete * 100.0 / total), else_=func.coalesce(AgentSnapshot.coverage_percent, 0))
|
||||
return func.round(case((raw < 0, 0), (raw > 100, 100), else_=raw), 1)
|
||||
|
||||
|
||||
def _fiscal_completion(snapshot) -> float:
|
||||
total = int(getattr(snapshot, "total_products", 0) or 0)
|
||||
complete = int(getattr(snapshot, "fiscally_complete", 0) or 0)
|
||||
if total:
|
||||
return round(max(0, min(total, complete)) * 100 / total, 1)
|
||||
return round(float(getattr(snapshot, "coverage_percent", 0) or 0), 1)
|
||||
return round(max(0, min(100, float(getattr(snapshot, "coverage_percent", 0) or 0))), 1)
|
||||
|
||||
|
||||
def _fiscal_status(snapshot) -> str:
|
||||
@@ -134,21 +159,20 @@ def _installation_query_filters(query, *, client=None, version=None, search=None
|
||||
|
||||
def list_installations(session, *, page=1, page_size=50, client=None, health=None, version=None, search=None, last_seen_before=None, sort="display_name", 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 = _installation_query_filters(query, client=client, version=version, search=search, last_seen_before=last_seen_before)
|
||||
health_expression = _health_expression(now)
|
||||
if health:
|
||||
query = query.where(health_expression == health)
|
||||
if sort in {"worst_coverage", "coverage", "coverage_asc", "fiscal_completion_percent"}:
|
||||
ordered = query.order_by(func.coalesce(AgentSnapshot.coverage_percent, 0).asc(), Installation.display_name.asc(), Installation.id.asc())
|
||||
ordered = query.order_by(_fiscal_completion_expression().asc(), Installation.display_name.asc(), Installation.id.asc())
|
||||
else:
|
||||
ordered = query.order_by(Installation.display_name.asc(), Installation.id.asc())
|
||||
rows = session.execute(ordered).all() if health else session.execute(ordered.offset((page - 1) * page_size).limit(page_size)).all()
|
||||
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]
|
||||
if health:
|
||||
items = [item for item in items if item["health"] == health]
|
||||
total = len(items)
|
||||
items = items[(page - 1) * page_size:page * page_size]
|
||||
else:
|
||||
count_query = select(func.count()).select_from(query.with_only_columns(Installation.id).order_by(None).subquery())
|
||||
total = session.scalar(count_query) or 0
|
||||
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}
|
||||
|
||||
|
||||
@@ -199,9 +223,7 @@ def _parse_age(value):
|
||||
|
||||
|
||||
def _blockers_query(session, *, tenant_id=None, agent_id=None, blocker_type=None, field=None, status="OPEN", client=None, installation=None, search=None, age=None):
|
||||
installation_filter = aliased(Installation)
|
||||
representative = select(func.min(Installation.id).label("installation_db_id"), Installation.tenant_id.label("installation_tenant_id")).group_by(Installation.tenant_id).subquery()
|
||||
query = select(BlockedProduct, Tenant, Installation, Agent).join(Tenant, Tenant.id == BlockedProduct.tenant_id).outerjoin(representative, representative.c.installation_tenant_id == BlockedProduct.tenant_id).outerjoin(Installation, Installation.id == representative.c.installation_db_id).outerjoin(Agent, Agent.id == Installation.agent_id)
|
||||
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)
|
||||
status = (status or "OPEN").upper()
|
||||
if status == "OPEN":
|
||||
query = query.where(BlockedProduct.resolved_at.is_(None))
|
||||
@@ -210,7 +232,7 @@ def _blockers_query(session, *, tenant_id=None, agent_id=None, blocker_type=None
|
||||
if tenant_id is not None:
|
||||
query = query.where(BlockedProduct.tenant_id == tenant_id)
|
||||
if agent_id is not None:
|
||||
query = query.where(exists(select(installation_filter.id).where(installation_filter.tenant_id == BlockedProduct.tenant_id, installation_filter.agent_id == agent_id)))
|
||||
query = query.where(Agent.id == agent_id)
|
||||
if blocker_type:
|
||||
query = query.where(BlockedProduct.blocker_type == blocker_type)
|
||||
if field:
|
||||
@@ -218,18 +240,7 @@ def _blockers_query(session, *, tenant_id=None, agent_id=None, blocker_type=None
|
||||
if client:
|
||||
query = query.where(or_(Tenant.name.ilike(f"%{client}%"), BlockedProduct.company.ilike(f"%{client}%")))
|
||||
if installation:
|
||||
query = query.where(
|
||||
exists(
|
||||
select(installation_filter.id).where(
|
||||
installation_filter.tenant_id == BlockedProduct.tenant_id,
|
||||
or_(
|
||||
installation_filter.installation_id.ilike(f"%{installation}%"),
|
||||
installation_filter.display_name.ilike(f"%{installation}%"),
|
||||
installation_filter.hostname.ilike(f"%{installation}%"),
|
||||
),
|
||||
)
|
||||
)
|
||||
)
|
||||
query = query.where(or_(Installation.installation_id.ilike(f"%{installation}%"), Installation.display_name.ilike(f"%{installation}%"), Installation.hostname.ilike(f"%{installation}%")))
|
||||
if search:
|
||||
term = f"%{search}%"
|
||||
query = query.where(or_(BlockedProduct.product_code.ilike(term), BlockedProduct.description.ilike(term), BlockedProduct.field.ilike(term), BlockedProduct.blocker_type.ilike(term), BlockedProduct.reason_code.ilike(term), Tenant.name.ilike(term)))
|
||||
@@ -300,50 +311,80 @@ def list_jobs(session, *, agent_id=None, page=1, page_size=50, status=None, kind
|
||||
|
||||
|
||||
def dashboard_summary(session, now=None):
|
||||
now = now or datetime.now(timezone.utc)
|
||||
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).order_by(Installation.id.asc())
|
||||
items = [installation_view(i, a, t, s, now=now) for i, a, t, s in session.execute(query).all()]
|
||||
counts = {state: sum(item["technical_health"] == state for item in items) for state in HEALTH_STATES}
|
||||
totals = {key: sum(int(item[key] or 0) for item in items) for key in ("total_products", "fiscally_complete", "safe_blocked", "failed_products")}
|
||||
blocker_counts = {}
|
||||
for item in items:
|
||||
for key, value in (item["blockers"] or {}).items():
|
||||
blocker_counts[key] = blocker_counts.get(key, 0) + int(value or 0)
|
||||
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)
|
||||
health_expression = _health_expression(now)
|
||||
health_rows = session.execute(
|
||||
select(health_expression.label("health"), func.count(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)
|
||||
.group_by(health_expression)
|
||||
).all()
|
||||
counts = {state: 0 for state in HEALTH_STATES}
|
||||
for state, count in health_rows:
|
||||
counts[state] = int(count)
|
||||
|
||||
blocker_rows = session.scalars(select(BlockedProduct).where(BlockedProduct.resolved_at.is_(None))).all()
|
||||
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),
|
||||
).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)))
|
||||
|
||||
open_filter = BlockedProduct.resolved_at.is_(None)
|
||||
open_blockers = session.scalar(select(func.count(BlockedProduct.id)).where(open_filter)) 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()
|
||||
open_blocker_breakdown = {field: 0 for field in BLOCKER_FIELDS}
|
||||
product_pairs = set()
|
||||
for row in blocker_rows:
|
||||
product_pairs.add((row.tenant_id, row.product_code))
|
||||
key = str(row.field or "").upper()
|
||||
open_blocker_breakdown[key if key in open_blocker_breakdown else "OTHER"] += 1
|
||||
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()
|
||||
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}
|
||||
|
||||
def installation_candidates(health):
|
||||
query = installation_query.where(health_expression == health).order_by(Installation.id.asc()).limit(ATTENTION_CANDIDATE_LIMIT)
|
||||
return [installation_view(i, a, t, s, now=now) for i, a, t, s in session.execute(query).all()]
|
||||
|
||||
health_candidates = {health: installation_candidates(health) for health in ("FAILED", "DEGRADED", "OFFLINE", "UNKNOWN")}
|
||||
version_filter = or_(AgentSnapshot.version_drift == "OUTDATED", and_(Agent.agent_version.is_not(None), Agent.agent_version != settings.agent_server_version))
|
||||
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, 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()
|
||||
|
||||
attention = []
|
||||
for item in items:
|
||||
if item["technical_health"] in {"FAILED", "DEGRADED", "OFFLINE", "UNKNOWN"}:
|
||||
rank = {"FAILED": 0, "DEGRADED": 1, "OFFLINE": 2, "UNKNOWN": 3}[item["technical_health"]]
|
||||
attention.append((_attention_item(item, item["health_reason"], _technical_timestamp(item), rank=rank, now=now), rank, item["agent_id"]))
|
||||
item_by_tenant = {item["tenant_id"]: item for item in items}
|
||||
for row in blocker_rows:
|
||||
item = item_by_tenant.get(row.tenant_id)
|
||||
if item and (row.field or "").upper() in BLOCKER_FIELDS[:-1]:
|
||||
attention.append((_attention_item(item, f"{row.field or row.blocker_type}: {row.reason_code}", row.last_seen_at, rank=4, link=f"/blockers/{row.id}", now=now), 4, row.id))
|
||||
for item in items:
|
||||
if item["version_drift"] == "OUTDATED":
|
||||
attention.append((_attention_item(item, "Versão do agent desatualizada", item.get("last_seen_at"), rank=5, now=now), 5, item["agent_id"]))
|
||||
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 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.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
|
||||
return {
|
||||
"agents_total": len(items),
|
||||
"agents_total": sum(counts.values()),
|
||||
**{key.lower(): value for key, value in counts.items()},
|
||||
"health_counts": counts,
|
||||
**totals,
|
||||
"blockers": blocker_counts,
|
||||
"stale_agents": [item for item in items if item["technical_health"] == "OFFLINE"],
|
||||
"outdated_agents": [item for item in items if item["version_drift"] == "OUTDATED"],
|
||||
"safe_blocked_products": len(product_pairs),
|
||||
"open_blockers": len(blocker_rows),
|
||||
"stale_agents": health_candidates["OFFLINE"],
|
||||
"outdated_agents": outdated_agents,
|
||||
"safe_blocked_products": int(safe_blocked_products),
|
||||
"open_blockers": int(open_blockers),
|
||||
"open_blocker_breakdown": open_blocker_breakdown,
|
||||
"attention_items": [entry[0] for entry in attention[:10]],
|
||||
"failed_jobs": failed_jobs,
|
||||
|
||||
Reference in New Issue
Block a user