"""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