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:
2026-04-06 20:58:48 +01:00
parent a4271fd06a
commit 12f688ff7c
23 changed files with 4123 additions and 21 deletions
+451
View File
@@ -0,0 +1,451 @@
/**
* AiOverview — Painel Stack IA (3 Camadas de Execução)
* Rota: /ai
* @author Descomplicar® | @link descomplicar.pt | @copyright 2026
*/
import { useState, useEffect, useCallback } from 'react'
import { motion } from 'framer-motion'
import {
Brain,
Cpu,
Bot,
Zap,
Shield,
Database,
BookOpen,
Layers,
RefreshCw,
CheckCircle2,
AlertTriangle,
Circle,
} from 'lucide-react'
// --- Types ---
interface AiLayerItem {
metric: string
value: number
detail: string
}
interface AiLayer {
name: string
label: string
items: AiLayerItem[]
}
interface TransversalSystem {
name: string
status: 'active' | 'warning' | 'inactive'
detail: string
}
interface CarlConfig {
domains: string[]
total_rules: number
}
interface AiDashboard {
layers: AiLayer[]
transversal: TransversalSystem[]
carl: CarlConfig
notebooks: number
last_updated: string
}
// --- Animation variants ---
const containerVariants = {
hidden: { opacity: 0 },
show: { opacity: 1, transition: { staggerChildren: 0.06 } },
}
const itemVariants = {
hidden: { opacity: 0, y: 20 },
show: {
opacity: 1,
y: 0,
transition: { type: 'spring' as const, stiffness: 300, damping: 30 },
},
}
// --- Configuração visual por camada ---
interface LayerConfig {
gradient: string
border: string
badge: string
valueCls: string
glow: string
icon: React.ElementType
}
const LAYER_CONFIG: Record<string, LayerConfig> = {
'Claude Code': {
gradient: 'from-violet-500/15 to-purple-500/10',
border: 'border-violet-500/25',
badge: 'bg-violet-500/20 text-violet-300 border border-violet-500/30',
valueCls: 'text-violet-300',
glow: 'shadow-[0_0_20px_rgba(139,92,246,0.15)]',
icon: Brain,
},
'n8n': {
gradient: 'from-amber-500/15 to-orange-500/10',
border: 'border-amber-500/25',
badge: 'bg-amber-500/20 text-amber-300 border border-amber-500/30',
valueCls: 'text-amber-300',
glow: 'shadow-[0_0_20px_rgba(245,158,11,0.15)]',
icon: Zap,
},
'Paperclip': {
gradient: 'from-cyan-500/15 to-teal-500/10',
border: 'border-cyan-500/25',
badge: 'bg-cyan-500/20 text-cyan-300 border border-cyan-500/30',
valueCls: 'text-cyan-300',
glow: 'shadow-[0_0_20px_rgba(6,182,212,0.15)]',
icon: Bot,
},
}
// --- Sub-components ---
const MetricCard = ({
item,
valueCls,
}: {
item: AiLayerItem
valueCls: string
}) => (
<motion.div
variants={itemVariants}
className="bg-white/5 border border-white/8 rounded-xl px-5 py-4 flex flex-col gap-1"
>
<span className="text-xs text-zinc-500 uppercase tracking-wide font-medium">
{item.metric}
</span>
<span className={`text-3xl font-bold ${valueCls}`}>{item.value}</span>
<span className="text-xs text-zinc-400">{item.detail}</span>
</motion.div>
)
const LayerSection = ({ layer }: { layer: AiLayer }) => {
const cfg = LAYER_CONFIG[layer.name] ?? LAYER_CONFIG['Claude Code']
const Icon = cfg.icon
return (
<motion.div
variants={itemVariants}
className={`glass-card overflow-hidden bg-gradient-to-br ${cfg.gradient} border ${cfg.border} ${cfg.glow}`}
>
{/* Cabeçalho da camada */}
<div className={`px-6 py-4 border-b ${cfg.border} flex items-center justify-between`}>
<div className="flex items-center gap-3">
<div className={`w-9 h-9 rounded-xl flex items-center justify-center ${cfg.badge}`}>
<Icon className="w-5 h-5" />
</div>
<div>
<h3 className="text-sm font-bold text-white uppercase tracking-wider">
{layer.name}
</h3>
<p className="text-xs text-zinc-500 mt-0.5">{layer.label}</p>
</div>
</div>
</div>
{/* Cards de métricas */}
<div className="p-6">
<motion.div
variants={containerVariants}
initial="hidden"
animate="show"
className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3"
>
{layer.items.map((item) => (
<MetricCard key={item.metric} item={item} valueCls={cfg.valueCls} />
))}
</motion.div>
</div>
</motion.div>
)
}
const TransversalStatusIcon = ({ status }: { status: string }) => {
if (status === 'active') {
return <CheckCircle2 className="w-4 h-4 text-emerald-400 shrink-0" />
}
if (status === 'warning') {
return <AlertTriangle className="w-4 h-4 text-amber-400 shrink-0" />
}
return <Circle className="w-4 h-4 text-zinc-600 shrink-0" />
}
const TransversalCard = ({ sys }: { sys: TransversalSystem }) => {
const borderCls =
sys.status === 'active'
? 'border-emerald-500/20'
: sys.status === 'warning'
? 'border-amber-500/20'
: 'border-white/8'
return (
<motion.div
variants={itemVariants}
className={`glass-card px-5 py-4 flex items-start gap-3 border ${borderCls}`}
>
<TransversalStatusIcon status={sys.status} />
<div className="min-w-0">
<p className="text-sm font-semibold text-white truncate">{sys.name}</p>
<p className="text-xs text-zinc-400 mt-0.5 leading-relaxed">{sys.detail}</p>
</div>
</motion.div>
)
}
const DomainPill = ({ name }: { name: string }) => (
<motion.span
variants={itemVariants}
className="px-3 py-1.5 rounded-full text-xs font-semibold uppercase tracking-wide
bg-violet-500/15 text-violet-300 border border-violet-500/25"
>
{name}
</motion.span>
)
// --- Página principal ---
export default function AiOverview() {
const [data, setData] = useState<AiDashboard | null>(null)
const [loading, setLoading] = useState(true)
const [refreshing, setRefreshing] = useState(false)
const fetchData = useCallback(async () => {
setRefreshing(true)
try {
const response = await fetch('/api/ai')
if (!response.ok) throw new Error('Failed to fetch /api/ai')
const json = await response.json()
setData(json)
} catch {
console.error('Falha ao carregar dados do stack IA')
} finally {
setLoading(false)
setRefreshing(false)
}
}, [])
useEffect(() => { fetchData() }, [fetchData])
const formatDate = (iso: string) => {
// Converter YYYY-MM-DD para DD-MM-YYYY
const [y, m, d] = iso.split('-')
return `${d}-${m}-${y}`
}
if (loading) {
return (
<div className="flex items-center justify-center h-96">
<div className="flex flex-col items-center gap-4">
<div className="w-10 h-10 border-2 border-violet-500/50 border-t-violet-400 rounded-full animate-spin" />
<p className="text-sm text-zinc-500">A carregar stack IA...</p>
</div>
</div>
)
}
if (!data) {
return (
<div className="flex items-center justify-center h-96">
<p className="text-sm text-zinc-500">Não foi possível carregar os dados.</p>
</div>
)
}
return (
<div className="space-y-8 pb-10">
{/* Cabeçalho */}
<motion.div
initial={{ opacity: 0, y: -16 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4 }}
className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4"
>
<div className="flex items-center gap-4">
<div className="w-12 h-12 rounded-2xl bg-violet-500/20 border border-violet-500/30
flex items-center justify-center shadow-[0_0_24px_rgba(139,92,246,0.2)]">
<Layers className="w-6 h-6 text-violet-400" />
</div>
<div>
<h1 className="text-2xl font-bold text-white">Stack IA 3 Camadas de Execução</h1>
<p className="text-sm text-zinc-500 mt-0.5">
Inventário completo do sistema IA Descomplicar®
</p>
</div>
</div>
<div className="flex items-center gap-3 shrink-0">
<span className="text-xs px-3 py-1.5 rounded-full bg-violet-500/10 border border-violet-500/20 text-violet-400">
Última actualização: {formatDate(data.last_updated)}
</span>
<button
onClick={fetchData}
disabled={refreshing}
className="flex items-center gap-2 px-4 py-2 rounded-xl bg-white/5 hover:bg-white/10
border border-white/10 text-sm text-zinc-300 transition-colors disabled:opacity-50"
>
<RefreshCw className={`w-4 h-4 ${refreshing ? 'animate-spin' : ''}`} />
Actualizar
</button>
</div>
</motion.div>
{/* Sumário global — pill badges */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.1 }}
className="flex flex-wrap gap-3"
>
<span className="px-3 py-1.5 rounded-full text-xs font-medium bg-white/5 border border-white/10 text-zinc-300">
<span className="text-violet-400 font-bold mr-1">{data.notebooks}</span>
Notebooks NotebookLM
</span>
<span className="px-3 py-1.5 rounded-full text-xs font-medium bg-white/5 border border-white/10 text-zinc-300">
<span className="text-violet-400 font-bold mr-1">{data.carl.total_rules}</span>
Regras CARL
</span>
<span className="px-3 py-1.5 rounded-full text-xs font-medium bg-white/5 border border-white/10 text-zinc-300">
<span className="text-violet-400 font-bold mr-1">{data.carl.domains.length}</span>
Domínios contextuais
</span>
</motion.div>
{/* 3 Camadas */}
<motion.div
variants={containerVariants}
initial="hidden"
animate="show"
className="space-y-4"
>
{data.layers.map((layer) => (
<LayerSection key={layer.name} layer={layer} />
))}
</motion.div>
{/* Sistemas Transversais */}
<div>
<motion.div
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
className="flex items-center gap-3 mb-4"
>
<Shield className="w-5 h-5 text-violet-400" />
<h2 className="text-sm font-bold text-white uppercase tracking-wider">
Sistemas Transversais
</h2>
<span className="text-xs px-2 py-0.5 rounded-full bg-white/5 border border-white/10 text-zinc-400">
{data.transversal.length} sistemas
</span>
</motion.div>
<motion.div
variants={containerVariants}
initial="hidden"
animate="show"
className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5 gap-3"
>
{data.transversal.map((sys) => (
<TransversalCard key={sys.name} sys={sys} />
))}
</motion.div>
</div>
{/* CARL — domínios contextuais */}
<div>
<motion.div
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
className="flex items-center gap-3 mb-4"
>
<Database className="w-5 h-5 text-violet-400" />
<h2 className="text-sm font-bold text-white uppercase tracking-wider">
CARL Contexto Adaptativo
</h2>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.35 }}
className="glass-card p-6 border border-violet-500/20
bg-gradient-to-br from-violet-500/10 to-purple-500/5"
>
<div className="flex items-start gap-6 flex-col sm:flex-row">
{/* Métricas CARL */}
<div className="flex gap-6 shrink-0">
<div className="text-center">
<p className="text-3xl font-bold text-violet-300">{data.carl.domains.length}</p>
<p className="text-xs text-zinc-500 mt-1 uppercase tracking-wide">Domínios</p>
</div>
<div className="w-px bg-white/10 self-stretch" />
<div className="text-center">
<p className="text-3xl font-bold text-violet-300">{data.carl.total_rules}</p>
<p className="text-xs text-zinc-500 mt-1 uppercase tracking-wide">Regras</p>
</div>
</div>
{/* Divisor */}
<div className="hidden sm:block w-px bg-white/10 self-stretch" />
{/* Domínios como pills */}
<motion.div
variants={containerVariants}
initial="hidden"
animate="show"
className="flex flex-wrap gap-2"
>
{data.carl.domains.map((domain) => (
<DomainPill key={domain} name={domain} />
))}
</motion.div>
</div>
</motion.div>
</div>
{/* Knowledge Notebooks */}
<div>
<motion.div
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4 }}
className="flex items-center gap-3 mb-4"
>
<BookOpen className="w-5 h-5 text-violet-400" />
<h2 className="text-sm font-bold text-white uppercase tracking-wider">
Knowledge Base
</h2>
</motion.div>
<motion.div
initial={{ opacity: 0, scale: 0.97 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.45 }}
className="glass-card p-6 border border-violet-500/15
bg-gradient-to-br from-violet-500/8 to-transparent
flex items-center gap-6"
>
<Cpu className="w-10 h-10 text-violet-400/60 shrink-0" />
<div>
<p className="text-4xl font-bold text-violet-300">{data.notebooks}</p>
<p className="text-sm text-zinc-400 mt-1">
Notebooks NotebookLM activos fonte de conhecimento para todos os agentes
</p>
</div>
</motion.div>
</div>
</div>
)
}