184 lines
6.2 KiB
Python
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
|