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>
This commit is contained in:
@@ -0,0 +1,209 @@
|
||||
/**
|
||||
* 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 }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user