Files
DashDescomplicar/api/services/paperclip.ts
ealmeida 12f688ff7c feat: adicionar 5 novos painéis ao dashboard (MCPs, n8n, Paperclip, IA, Operações)
Expansão do dashboard de 3 para 8 páginas com dados reais do stack:
- MCPs: monitorização de 33 MCPs no gateway com ping e estado online/offline
- n8n: 14 workflows com último run, duração e falhas 24h
- Paperclip: 16 agentes operacionais, routines e issues (PostgreSQL)
- IA/Claude: visão das 3 camadas (189 skills, 72 agents, 39 MCPs, CARL)
- Operações: tickets Desk CRM por departamento + cobertura PROCs

16 ficheiros novos (3042 linhas), 3 existentes editados.
Nova dependência: pg (PostgreSQL client para Paperclip).
Audit: 0 vulnerabilidades (npm audit fix aplicado).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 20:58:48 +01:00

210 lines
6.1 KiB
TypeScript

/**
* Paperclip Service — Queries ao PostgreSQL do Paperclip
* @author Descomplicar® | @link descomplicar.pt | @copyright 2026
*
* NOTA SOBRE ESQUEMA DA BD:
* Os nomes de tabelas e colunas abaixo são baseados na spec (SPEC-dashboard-expansion-q2-2026.md §4.3).
* Se o esquema real da BD Paperclip diferir, ajustar as queries:
* - Tabela de agentes: 'agents' (colunas: name, role, status, last_heartbeat)
* - Tabela de routines: 'routines' (colunas: name, cron_expression, enabled, last_run_at, last_run_status)
* - Tabela de issues: 'issues' (colunas: status, closed_at)
* Verificar schema real com: \dt e \d <tabela> no psql.
*/
import pool from './paperclip-db.js'
// ---------------------------------------------------------------------------
// Types (espelham a spec)
// ---------------------------------------------------------------------------
export interface PaperclipAgent {
id: string
name: string
role: string
status: 'active' | 'idle' | 'error' | 'archived'
last_heartbeat: string | null
last_run: string | null
total_runs: number
}
export interface PaperclipRoutine {
id: string
name: string
cron: string
active: boolean
last_run: string | null
last_status: 'success' | 'error' | null
next_run: string | null
}
export interface PaperclipDashboard {
agents: {
total: number
active: number
idle: number
error: number
list: PaperclipAgent[]
}
routines: {
total: number
active: number
list: PaperclipRoutine[]
}
issues: {
open: number
in_progress: number
closed_7d: number
}
}
// ---------------------------------------------------------------------------
// Dados de fallback (quando BD não está configurada ou inacessível)
// ---------------------------------------------------------------------------
const FALLBACK: PaperclipDashboard = {
agents: {
total: 0,
active: 0,
idle: 0,
error: 0,
list: [],
},
routines: {
total: 0,
active: 0,
list: [],
},
issues: {
open: 0,
in_progress: 0,
closed_7d: 0,
},
}
// ---------------------------------------------------------------------------
// Função principal — todas as queries em paralelo
// ---------------------------------------------------------------------------
export async function getPaperclipDashboard(): Promise<PaperclipDashboard> {
// Se pool não foi configurado (credenciais em falta), retornar fallback
if (!pool) {
return { ...FALLBACK }
}
try {
const [agentsResult, routinesResult, issuesOpenResult, issuesInProgressResult, issuesClosedResult] =
await Promise.all([
// Query A: Agentes activos (excluindo archived)
// NOTA: colunas last_run e total_runs podem não existir — ajustar se necessário
pool.query<{
id: string
name: string
role: string
status: string
last_heartbeat: string | null
last_run: string | null
total_runs: number
}>(
`SELECT
id,
name,
role,
status,
last_heartbeat,
last_run_at AS last_run,
COALESCE(total_runs, 0) AS total_runs
FROM agents
WHERE status != 'archived'
ORDER BY role, name`
),
// Query B: Routines ordenadas por nome
pool.query<{
id: string
name: string
cron: string
active: boolean
last_run: string | null
last_status: string | null
next_run: string | null
}>(
`SELECT
id,
name,
cron_expression AS cron,
enabled AS active,
last_run_at AS last_run,
last_run_status AS last_status,
next_run_at AS next_run
FROM routines
ORDER BY name`
),
// Query C1: Issues abertas
pool.query<{ count: string }>(
`SELECT COUNT(*) AS count FROM issues WHERE status = 'open'`
),
// Query C2: Issues em progresso
pool.query<{ count: string }>(
`SELECT COUNT(*) AS count FROM issues WHERE status = 'in_progress'`
),
// Query C3: Issues fechadas nos últimos 7 dias
pool.query<{ count: string }>(
`SELECT COUNT(*) AS count FROM issues WHERE status = 'closed' AND closed_at > NOW() - INTERVAL '7 days'`
),
])
const agents = agentsResult.rows
const routines = routinesResult.rows
// Contagens de agentes por estado
const activeCount = agents.filter(a => a.status === 'active').length
const idleCount = agents.filter(a => a.status === 'idle').length
const errorCount = agents.filter(a => a.status === 'error').length
// Contagens de routines activas
const activeRoutines = routines.filter(r => r.active).length
return {
agents: {
total: agents.length,
active: activeCount,
idle: idleCount,
error: errorCount,
list: agents.map(a => ({
id: a.id ?? '',
name: a.name,
role: a.role,
status: a.status as PaperclipAgent['status'],
last_heartbeat: a.last_heartbeat ?? null,
last_run: a.last_run ?? null,
total_runs: Number(a.total_runs) || 0,
})),
},
routines: {
total: routines.length,
active: activeRoutines,
list: routines.map(r => ({
id: r.id ?? '',
name: r.name,
cron: r.cron,
active: Boolean(r.active),
last_run: r.last_run ?? null,
last_status: (r.last_status as PaperclipRoutine['last_status']) ?? null,
next_run: r.next_run ?? null,
})),
},
issues: {
open: parseInt(issuesOpenResult.rows[0]?.count ?? '0', 10),
in_progress: parseInt(issuesInProgressResult.rows[0]?.count ?? '0', 10),
closed_7d: parseInt(issuesClosedResult.rows[0]?.count ?? '0', 10),
},
}
} catch (err) {
console.error('[paperclip] Erro ao obter dados da BD Paperclip:', (err as Error).message)
// Retornar fallback em caso de falha de BD (sem rebentar a API)
return { ...FALLBACK }
}
}