ligbox-ops-platform/projects/ops-desk/api/app/ops_inbox_store.py
Ligbox Spec Hub b03bb2c37c feat(desk): UI/API Spec 039-041 + deploy atómico e smoke GREEN
Commita governance, user-wizard, operational-feed e catálogo RBAC;
adiciona deploy-desk-full.sh, smoke-desk.sh e regra anti-deploy parcial;
documenta credencial VM112 @betinplace.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-02 13:32:15 +00:00

338 lines
12 KiB
Python

"""Operational feed mock store. Spec 041 · OF-API-001."""
from __future__ import annotations
import json
import sqlite3
from datetime import datetime, timezone, timedelta
from typing import Any
CHANNELS = (
("all", "Todos os canais"),
("email", "Email"),
("whatsapp", "WhatsApp API"),
("voice", "Telefonia / Voz"),
("sms", "SMS"),
("telegram", "Telegram"),
("tickets", "Tickets"),
("agents", "Agentes IA"),
("internal", "Solicitações internas"),
("clients", "Clientes"),
("alerts", "Alertas sistema"),
)
PRIORITIES = ("normal", "high", "critical")
def _now() -> str:
return datetime.now(timezone.utc).isoformat()
def _minutes_ago(minutes: int) -> str:
return (datetime.now(timezone.utc) - timedelta(minutes=minutes)).isoformat()
def init_inbox_schema(conn: sqlite3.Connection) -> None:
conn.executescript(
"""
CREATE TABLE IF NOT EXISTS ops_inbox_events (
id TEXT PRIMARY KEY,
channel TEXT NOT NULL,
event_type TEXT NOT NULL,
priority TEXT NOT NULL DEFAULT 'normal',
title TEXT NOT NULL,
preview TEXT NOT NULL,
tags_json TEXT NOT NULL DEFAULT '[]',
assignee TEXT,
status TEXT NOT NULL DEFAULT 'open',
contact_name TEXT,
contact_company TEXT,
contact_cnpj TEXT,
contact_client_id TEXT,
sla_minutes INTEGER NOT NULL DEFAULT 60,
sla_remaining_sec INTEGER NOT NULL DEFAULT 3600,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS ops_inbox_messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
event_id TEXT NOT NULL,
author_type TEXT NOT NULL,
author_label TEXT NOT NULL,
body TEXT NOT NULL,
created_at TEXT NOT NULL,
FOREIGN KEY (event_id) REFERENCES ops_inbox_events(id)
);
CREATE INDEX IF NOT EXISTS idx_ops_inbox_events_channel ON ops_inbox_events(channel);
CREATE INDEX IF NOT EXISTS idx_ops_inbox_events_status ON ops_inbox_events(status);
"""
)
count = conn.execute("SELECT COUNT(*) c FROM ops_inbox_events").fetchone()["c"]
if count == 0:
_seed_inbox(conn)
def _seed_inbox(conn: sqlite3.Connection) -> None:
now = _now()
seeds = [
{
"id": "evt-wa-001",
"channel": "whatsapp",
"event_type": "message",
"priority": "high",
"title": "Cliente: Empresa Alpha — Não consigo acessar o painel financeiro",
"preview": "Bom dia, após o login aparece erro 403 no módulo billing…",
"tags": ["Cliente", "Acesso", "Infraestrutura"],
"assignee": "Editor",
"status": "open",
"contact_name": "Empresa Alpha",
"contact_company": "Empresa Alpha Ltda",
"contact_cnpj": "12.345.678/0001-90",
"contact_client_id": "CLI-8842",
"sla_minutes": 15,
"sla_remaining_sec": 750,
"created_at": _minutes_ago(2),
},
{
"id": "evt-int-002",
"channel": "internal",
"event_type": "request",
"priority": "normal",
"title": "Carlos — Financeiro: Aprovar reembolso #8821",
"preview": "Solicitação interna aguardando aprovação do Chefe Ops",
"tags": ["Interno", "Financeiro"],
"assignee": "Super Admin",
"status": "pending",
"contact_name": "Carlos Mendes",
"contact_company": "Ligbox Ops",
"contact_cnpj": "",
"contact_client_id": "",
"sla_minutes": 120,
"sla_remaining_sec": 5400,
"created_at": _minutes_ago(18),
},
{
"id": "evt-ai-003",
"channel": "agents",
"event_type": "alert",
"priority": "critical",
"title": "Agente A3 — CPU VM122 acima de 92%",
"preview": "Alerta automático: load average 4.2 — recomendação de escala",
"tags": ["Agente IA", "Infra", "VM122"],
"assignee": "AI Agent",
"status": "open",
"contact_name": "Watchman A3",
"contact_company": "Ligbox Platform",
"contact_cnpj": "",
"contact_client_id": "",
"sla_minutes": 30,
"sla_remaining_sec": 1200,
"created_at": _minutes_ago(5),
},
{
"id": "evt-em-004",
"channel": "email",
"event_type": "message",
"priority": "normal",
"title": "Empresa Beta — Pedido de upgrade de plano",
"preview": "Gostaríamos de migrar para o plano enterprise…",
"tags": ["Cliente", "Comercial"],
"assignee": "Sales Admin",
"status": "open",
"contact_name": "Empresa Beta",
"contact_company": "Beta Serviços SA",
"contact_cnpj": "98.765.432/0001-10",
"contact_client_id": "CLI-1201",
"sla_minutes": 240,
"sla_remaining_sec": 12000,
"created_at": _minutes_ago(45),
},
{
"id": "evt-tk-005",
"channel": "tickets",
"event_type": "ticket",
"priority": "high",
"title": "Ticket #4412 — Erro importação DNS",
"preview": "Falha ao sincronizar zona dns.example.com",
"tags": ["Ticket", "DNS"],
"assignee": "NOC",
"status": "open",
"contact_name": "Suporte N1",
"contact_company": "Cliente Gamma",
"contact_cnpj": "",
"contact_client_id": "CLI-3300",
"sla_minutes": 60,
"sla_remaining_sec": 2100,
"created_at": _minutes_ago(12),
},
{
"id": "evt-vc-006",
"channel": "voice",
"event_type": "missed_call",
"priority": "normal",
"title": "Chamada perdida — +55 11 98765-4321",
"preview": "Duração 0s · fila comercial",
"tags": ["Telefonia"],
"assignee": None,
"status": "open",
"contact_name": "Desconhecido",
"contact_company": "",
"contact_cnpj": "",
"contact_client_id": "",
"sla_minutes": 30,
"sla_remaining_sec": 900,
"created_at": _minutes_ago(8),
},
]
for s in seeds:
conn.execute(
"""
INSERT INTO ops_inbox_events
(id, channel, event_type, priority, title, preview, tags_json, assignee, status,
contact_name, contact_company, contact_cnpj, contact_client_id,
sla_minutes, sla_remaining_sec, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
s["id"], s["channel"], s["event_type"], s["priority"], s["title"], s["preview"],
json.dumps(s["tags"]), s["assignee"], s["status"],
s["contact_name"], s["contact_company"], s["contact_cnpj"], s["contact_client_id"],
s["sla_minutes"], s["sla_remaining_sec"], s["created_at"], s["created_at"],
),
)
msgs = [
("evt-wa-001", "user", "Empresa Alpha", "Bom dia, não consigo acessar o painel financeiro."),
("evt-wa-001", "agent", "AI Agent", "Detectei erro 403 no módulo billing. Encaminhei para suporte."),
("evt-wa-001", "system", "Sistema", "SLA iniciado — 15 min"),
]
for event_id, author_type, author_label, body in msgs:
conn.execute(
"""
INSERT INTO ops_inbox_messages (event_id, author_type, author_label, body, created_at)
VALUES (?, ?, ?, ?, ?)
""",
(event_id, author_type, author_label, body, now),
)
def inbox_stats(conn: sqlite3.Connection) -> dict:
total = conn.execute("SELECT COUNT(*) c FROM ops_inbox_events").fetchone()["c"]
pending = conn.execute(
"SELECT COUNT(*) c FROM ops_inbox_events WHERE status IN ('open','pending')"
).fetchone()["c"]
critical = conn.execute(
"SELECT COUNT(*) c FROM ops_inbox_events WHERE priority = 'critical'"
).fetchone()["c"]
channels = []
for cid, label in CHANNELS:
if cid == "all":
cnt = total
else:
cnt = conn.execute(
"SELECT COUNT(*) c FROM ops_inbox_events WHERE channel = ?",
(cid,),
).fetchone()["c"]
channels.append({"id": cid, "label": label, "count": cnt})
return {
"events_today": total,
"pending": pending,
"critical": critical,
"awaiting_you": conn.execute(
"SELECT COUNT(*) c FROM ops_inbox_events WHERE assignee IS NOT NULL AND status = 'open'"
).fetchone()["c"],
"sla_avg_pct": 96,
"channels": channels,
}
def list_events(
conn: sqlite3.Connection,
*,
channel: str | None = None,
priority: str | None = None,
status: str | None = None,
q: str | None = None,
limit: int = 128,
) -> list[dict]:
sql = "SELECT * FROM ops_inbox_events WHERE 1=1"
params: list[Any] = []
if channel and channel != "all":
sql += " AND channel = ?"
params.append(channel)
if priority:
sql += " AND priority = ?"
params.append(priority)
if status:
sql += " AND status = ?"
params.append(status)
if q:
sql += " AND (title LIKE ? OR preview LIKE ? OR contact_name LIKE ?)"
like = f"%{q}%"
params.extend([like, like, like])
sql += " ORDER BY datetime(created_at) DESC LIMIT ?"
params.append(limit)
rows = conn.execute(sql, params).fetchall()
out = []
for row in rows:
item = dict(row)
try:
item["tags"] = json.loads(item.pop("tags_json") or "[]")
except json.JSONDecodeError:
item["tags"] = []
out.append(item)
return out
def get_event(conn: sqlite3.Connection, event_id: str) -> dict | None:
row = conn.execute("SELECT * FROM ops_inbox_events WHERE id = ?", (event_id,)).fetchone()
if not row:
return None
item = dict(row)
item["tags"] = json.loads(item.pop("tags_json") or "[]")
msgs = conn.execute(
"""
SELECT id, author_type, author_label, body, created_at
FROM ops_inbox_messages WHERE event_id = ? ORDER BY id ASC
""",
(event_id,),
).fetchall()
item["messages"] = [dict(m) for m in msgs]
return item
def add_message(conn: sqlite3.Connection, event_id: str, author_type: str, author_label: str, body: str) -> dict:
now = _now()
cur = conn.execute(
"""
INSERT INTO ops_inbox_messages (event_id, author_type, author_label, body, created_at)
VALUES (?, ?, ?, ?, ?)
""",
(event_id, author_type, author_label, body.strip(), now),
)
conn.execute(
"UPDATE ops_inbox_events SET updated_at = ? WHERE id = ?",
(now, event_id),
)
return {"id": cur.lastrowid, "event_id": event_id, "author_type": author_type,
"author_label": author_label, "body": body.strip(), "created_at": now}
def patch_event(conn: sqlite3.Connection, event_id: str, **fields: Any) -> dict | None:
allowed = {"status", "assignee", "priority"}
updates = []
params: list[Any] = []
for k, v in fields.items():
if k in allowed and v is not None:
updates.append(f"{k} = ?")
params.append(v)
if not updates:
ev = get_event(conn, event_id)
return ev
updates.append("updated_at = ?")
params.append(_now())
params.append(event_id)
conn.execute(
f"UPDATE ops_inbox_events SET {', '.join(updates)} WHERE id = ?",
params,
)
return get_event(conn, event_id)