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,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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user