ligbox-ops-platform/api/app/assist_catalog.py
Ligbox Spec Hub 3a2c64834b Initial import: ligbox-ops-platform + specs + LAPTOP + obsidian merge (CT130)
Source: VM122 /opt + obsidian-infra + LAPTOP
Hub: CT130 spec-hub 10.10.10.130
2026-06-19 17:26:41 +00:00

184 lines
6.2 KiB
Python

"""Catálogo de acções Desk + links externos + ranking técnicos — Spec 010 Fase C/F."""
from __future__ import annotations
import os
import sqlite3
from collections import defaultdict
from datetime import datetime, timedelta, timezone
from typing import Any
FUNNEL_RANK_BY_STAGE = {
"started": 1,
"domain_validated": 2,
"dns_applied": 3,
"account_created": 4,
"infra_synced": 5,
"completed": 6,
"failed": 99,
}
DESK_ACTIONS = {
"dns.revalidate": {
"label": "Revalidar DNS",
"min_rank": 3,
"method": "GET",
"path": "/onboarding/dns/verify/{domain}",
},
"dns.reapply": {
"label": "Reaplicar DNS Cloudflare",
"min_rank": 3,
"method": "POST",
"path": "/onboarding/dns/cloudflare/apply",
"body": lambda domain, _payload: {"domain": domain},
},
"account.retry_sync": {
"label": "Reverificar infra/conta",
"min_rank": 4,
"method": "GET",
"path": "/onboarding/infrastructure/status/{domain}",
},
"infra.resync": {
"label": "Resync infra (Traefik/cert)",
"min_rank": 5,
"method": "POST",
"path": "/onboarding/infrastructure/provision",
"body": lambda domain, _payload: {"domain": domain},
},
}
ABORT_ACTION = "onboarding.abort"
MARK_STEP_ACTION = "onboarding.mark_step_complete"
PROXMOX_URL = os.getenv("DESK_LINK_PROXMOX", "https://proxmox.itecnologys.com")
TRAEFIK_URL = os.getenv("DESK_LINK_TRAEFIK", "https://traefik.itecnologys.com/dashboard/")
CARBONIO_ADMIN = os.getenv("DESK_LINK_CARBONIO", "https://mail.ibytera.com/admin")
def _now() -> str:
return datetime.now(timezone.utc).isoformat()
def funnel_rank(stage: str | None) -> int:
return FUNNEL_RANK_BY_STAGE.get(stage or "", 0)
def list_actions_for_session(funnel_stage: str | None, is_assisting: bool, role: str) -> list[dict]:
rank = funnel_rank(funnel_stage)
out = []
for action_id, spec in DESK_ACTIONS.items():
if rank < spec["min_rank"]:
continue
if not is_assisting and action_id not in ("dns.revalidate", "account.retry_sync"):
continue
out.append({"id": action_id, "label": spec["label"], "requires_assisting": action_id not in ("dns.revalidate", "account.retry_sync")})
if is_assisting and role in ("super_admin", "ops_lead", "technician"):
out.append({"id": MARK_STEP_ACTION, "label": "Marcar passo concluído", "requires_assisting": True})
if role in ("super_admin", "ops_lead"):
out.append({"id": ABORT_ACTION, "label": "Abortar onboarding", "requires_assisting": False, "danger": True})
return out
def build_vm112_request(action_id: str, domain: str, ticket_payload: dict | None) -> tuple[str, str, dict | None]:
if action_id not in DESK_ACTIONS:
raise ValueError(f"acção desconhecida: {action_id}")
spec = DESK_ACTIONS[action_id]
path = spec["path"].format(domain=domain)
body = None
if "body" in spec:
body = spec["body"](domain, ticket_payload or {})
return spec["method"], path, body
def external_links(domain: str | None) -> list[dict[str, str]]:
dom = (domain or "").strip().lower()
links = [
{"id": "proxmox", "label": "Proxmox", "url": PROXMOX_URL, "system": "proxmox"},
{"id": "traefik", "label": "Traefik", "url": TRAEFIK_URL, "system": "traefik"},
{"id": "carbonio", "label": "Carbonio Admin", "url": CARBONIO_ADMIN, "system": "carbonio"},
]
if dom:
links.append({
"id": "webmail",
"label": f"Webmail {dom}",
"url": f"https://mail.{dom}/",
"system": "carbonio",
})
links.append({
"id": "cloudflare",
"label": f"Cloudflare DNS ({dom})",
"url": f"https://dash.cloudflare.com/?to=/:{dom}/dns",
"system": "cloudflare",
})
return links
def technician_ranking(conn: sqlite3.Connection, window_days: int = 30) -> list[dict[str, Any]]:
cutoff = (datetime.now(timezone.utc) - timedelta(days=window_days)).isoformat()
rows = conn.execute(
"""
SELECT aa.actor, aa.action, aa.created_at, s.initiated_by_user
FROM assist_actions aa
LEFT JOIN assist_sessions s ON s.id = aa.assist_session_id
WHERE aa.created_at >= ?
ORDER BY aa.id DESC
""",
(cutoff,),
).fetchall()
skip_actors = frozenset({"client", "system", "worker", ""})
stats: dict[str, dict[str, int]] = defaultdict(
lambda: {"escalados": 0, "assumidos": 0, "handoffs": 0, "acoes": 0, "movimentos": 0}
)
for row in rows:
actor = (row["actor"] or "").strip()
if actor in skip_actors:
continue
action = row["action"] or ""
bucket = stats[actor]
bucket["movimentos"] += 1
if action == "escalate":
bucket["escalados"] += 1
elif action == "takeover":
bucket["assumidos"] += 1
elif action == "handoff":
bucket["handoffs"] += 1
elif action.startswith("action."):
bucket["acoes"] += 1
assigned = conn.execute(
"""
SELECT assigned_to, COUNT(*) c
FROM tickets
WHERE assigned_to IS NOT NULL AND assigned_at >= ?
GROUP BY assigned_to
""",
(cutoff,),
).fetchall()
for row in assigned:
user = (row["assigned_to"] or "").strip()
if user:
stats[user]["atribuidos"] = int(row["c"])
ranking = []
for username, data in stats.items():
assumidos = data["assumidos"]
escalados = data["escalados"]
handoffs = data["handoffs"]
acoes = data["acoes"]
movimentos = data["movimentos"]
atribuidos = data.get("atribuidos", 0)
score = assumidos * 5 + escalados * 2 + acoes * 3 + handoffs + atribuidos
ranking.append({
"username": username,
"assumidos": assumidos,
"escalados": escalados,
"handoffs": handoffs,
"acoes": acoes,
"movimentos": movimentos,
"atribuidos": atribuidos,
"score": score,
})
ranking.sort(key=lambda x: (x["score"], x["assumidos"], x["movimentos"]), reverse=True)
return ranking