12f688ff7c
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>
210 lines
6.1 KiB
TypeScript
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 }
|
|
}
|
|
}
|