feat: Add complete dashboard for customers

🎨 Dashboard UI (Next.js 14)
- Modern dark theme with purple/green accents
- Responsive layout with sidebar navigation
- Real-time metrics cards with alerts
- Interactive charts (CPU, Memory) with Recharts
- Hosts table with status indicators
- Alerts list with AI suggestions

🤖 AI Copilot Chat
- Slide-in panel with chat interface
- Quick action buttons
- Command suggestions with execute option
- Real-time loading indicators

📊 Components
- MetricCard with trend indicators
- HostsTable with colored metrics
- AlertsList with severity levels
- AIInsights panel with predictions
- CpuChart and MemoryChart

🛠️ Tech Stack
- Next.js 14 with App Router
- TypeScript
- Tailwind CSS
- Recharts for visualization
- Zustand for state management
This commit is contained in:
2026-02-05 22:52:55 -03:00
parent 0f7ac29f6e
commit dbf9f0497f
17 changed files with 1138 additions and 0 deletions

6
dashboard/next.config.js Normal file
View File

@@ -0,0 +1,6 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'standalone',
}
module.exports = nextConfig

33
dashboard/package.json Normal file
View File

@@ -0,0 +1,33 @@
{
"name": "ophion-dashboard",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "next dev -p 3000",
"build": "next build",
"start": "next start -p 3000",
"lint": "next lint"
},
"dependencies": {
"next": "14.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"next-auth": "^4.24.0",
"@tanstack/react-query": "^5.17.0",
"recharts": "^2.10.0",
"lucide-react": "^0.312.0",
"clsx": "^2.1.0",
"tailwind-merge": "^2.2.0",
"date-fns": "^3.2.0",
"zustand": "^4.4.0"
},
"devDependencies": {
"typescript": "^5.3.0",
"@types/node": "^20.11.0",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"autoprefixer": "^10.4.0",
"postcss": "^8.4.0",
"tailwindcss": "^3.4.0"
}
}

View File

@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@@ -0,0 +1,63 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--background: 2 6 23;
--foreground: 248 250 252;
--card: 15 23 42;
--card-foreground: 248 250 252;
--primary: 34 197 94;
--primary-foreground: 255 255 255;
--secondary: 139 92 246;
--muted: 51 65 85;
--muted-foreground: 148 163 184;
--accent: 34 197 94;
--destructive: 239 68 68;
--border: 51 65 85;
--ring: 34 197 94;
}
body {
background: rgb(var(--background));
color: rgb(var(--foreground));
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: rgb(15, 23, 42);
}
::-webkit-scrollbar-thumb {
background: rgb(51, 65, 85);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: rgb(71, 85, 105);
}
/* Animations */
@keyframes pulse-glow {
0%, 100% { box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.4); }
50% { box-shadow: 0 0 0 8px rgba(34, 197, 94, 0); }
}
.pulse-glow {
animation: pulse-glow 2s infinite;
}
/* Card hover effects */
.card-hover {
transition: all 0.2s ease;
}
.card-hover:hover {
transform: translateY(-2px);
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
}

View File

@@ -0,0 +1,24 @@
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import './globals.css'
const inter = Inter({ subsets: ['latin'] })
export const metadata: Metadata = {
title: 'OPHION Dashboard',
description: 'Observability Platform with AI',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="pt-BR" className="dark">
<body className={`${inter.className} bg-slate-950 text-white`}>
{children}
</body>
</html>
)
}

134
dashboard/src/app/page.tsx Normal file
View File

@@ -0,0 +1,134 @@
'use client'
import { useState, useEffect } from 'react'
import Sidebar from '@/components/layout/Sidebar'
import Header from '@/components/layout/Header'
import MetricCard from '@/components/ui/MetricCard'
import HostsTable from '@/components/ui/HostsTable'
import AlertsList from '@/components/ui/AlertsList'
import CpuChart from '@/components/charts/CpuChart'
import MemoryChart from '@/components/charts/MemoryChart'
import AIInsights from '@/components/ui/AIInsights'
import Copilot from '@/components/ui/Copilot'
export default function Dashboard() {
const [showCopilot, setShowCopilot] = useState(false)
const [metrics, setMetrics] = useState({
totalHosts: 0,
healthyHosts: 0,
activeAlerts: 0,
cpuAvg: 0,
memoryAvg: 0,
diskAvg: 0,
})
// Simulated data - replace with real API calls
useEffect(() => {
setMetrics({
totalHosts: 12,
healthyHosts: 11,
activeAlerts: 3,
cpuAvg: 42.5,
memoryAvg: 68.3,
diskAvg: 54.2,
})
}, [])
return (
<div className="flex h-screen overflow-hidden">
{/* Sidebar */}
<Sidebar />
{/* Main Content */}
<div className="flex-1 flex flex-col overflow-hidden">
{/* Header */}
<Header onCopilotClick={() => setShowCopilot(true)} />
{/* Dashboard Content */}
<main className="flex-1 overflow-y-auto p-6 bg-slate-950">
{/* Top Metrics */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<MetricCard
title="Total Hosts"
value={metrics.totalHosts}
subtitle={`${metrics.healthyHosts} healthy`}
icon="🖥️"
trend="stable"
/>
<MetricCard
title="CPU Average"
value={`${metrics.cpuAvg}%`}
subtitle="across all hosts"
icon="⚡"
trend={metrics.cpuAvg > 70 ? 'up' : 'stable'}
alert={metrics.cpuAvg > 80}
/>
<MetricCard
title="Memory Average"
value={`${metrics.memoryAvg}%`}
subtitle="across all hosts"
icon="💾"
trend={metrics.memoryAvg > 70 ? 'up' : 'stable'}
alert={metrics.memoryAvg > 85}
/>
<MetricCard
title="Active Alerts"
value={metrics.activeAlerts}
subtitle="2 critical, 1 warning"
icon="🚨"
trend={metrics.activeAlerts > 0 ? 'up' : 'down'}
alert={metrics.activeAlerts > 0}
/>
</div>
{/* Charts Row */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<div className="bg-slate-900 rounded-xl p-6 border border-slate-800">
<h3 className="text-lg font-semibold mb-4">CPU Usage (24h)</h3>
<CpuChart />
</div>
<div className="bg-slate-900 rounded-xl p-6 border border-slate-800">
<h3 className="text-lg font-semibold mb-4">Memory Usage (24h)</h3>
<MemoryChart />
</div>
</div>
{/* AI Insights */}
<div className="mb-6">
<AIInsights />
</div>
{/* Bottom Section */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Hosts Table */}
<div className="bg-slate-900 rounded-xl p-6 border border-slate-800">
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-semibold">Hosts</h3>
<button className="text-sm text-green-400 hover:text-green-300">
View all
</button>
</div>
<HostsTable />
</div>
{/* Alerts */}
<div className="bg-slate-900 rounded-xl p-6 border border-slate-800">
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-semibold">Recent Alerts</h3>
<button className="text-sm text-green-400 hover:text-green-300">
View all
</button>
</div>
<AlertsList />
</div>
</div>
</main>
</div>
{/* AI Copilot Sidebar */}
{showCopilot && (
<Copilot onClose={() => setShowCopilot(false)} />
)}
</div>
)
}

View File

@@ -0,0 +1,64 @@
'use client'
import { LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer, Area, AreaChart } from 'recharts'
const data = [
{ time: '00:00', value: 35 },
{ time: '02:00', value: 28 },
{ time: '04:00', value: 22 },
{ time: '06:00', value: 25 },
{ time: '08:00', value: 45 },
{ time: '10:00', value: 62 },
{ time: '12:00', value: 58 },
{ time: '14:00', value: 72 },
{ time: '16:00', value: 68 },
{ time: '18:00', value: 55 },
{ time: '20:00', value: 48 },
{ time: '22:00', value: 42 },
]
export default function CpuChart() {
return (
<ResponsiveContainer width="100%" height={200}>
<AreaChart data={data}>
<defs>
<linearGradient id="cpuGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#22c55e" stopOpacity={0.3} />
<stop offset="95%" stopColor="#22c55e" stopOpacity={0} />
</linearGradient>
</defs>
<XAxis
dataKey="time"
stroke="#64748b"
fontSize={12}
tickLine={false}
axisLine={false}
/>
<YAxis
stroke="#64748b"
fontSize={12}
tickLine={false}
axisLine={false}
tickFormatter={(value) => `${value}%`}
domain={[0, 100]}
/>
<Tooltip
contentStyle={{
backgroundColor: '#1e293b',
border: '1px solid #334155',
borderRadius: '8px',
}}
labelStyle={{ color: '#94a3b8' }}
formatter={(value: number) => [`${value}%`, 'CPU']}
/>
<Area
type="monotone"
dataKey="value"
stroke="#22c55e"
strokeWidth={2}
fill="url(#cpuGradient)"
/>
</AreaChart>
</ResponsiveContainer>
)
}

View File

@@ -0,0 +1,64 @@
'use client'
import { AreaChart, Area, XAxis, YAxis, Tooltip, ResponsiveContainer } from 'recharts'
const data = [
{ time: '00:00', value: 58 },
{ time: '02:00', value: 55 },
{ time: '04:00', value: 52 },
{ time: '06:00', value: 54 },
{ time: '08:00', value: 62 },
{ time: '10:00', value: 68 },
{ time: '12:00', value: 72 },
{ time: '14:00', value: 78 },
{ time: '16:00', value: 75 },
{ time: '18:00', value: 70 },
{ time: '20:00', value: 65 },
{ time: '22:00', value: 60 },
]
export default function MemoryChart() {
return (
<ResponsiveContainer width="100%" height={200}>
<AreaChart data={data}>
<defs>
<linearGradient id="memoryGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#8b5cf6" stopOpacity={0.3} />
<stop offset="95%" stopColor="#8b5cf6" stopOpacity={0} />
</linearGradient>
</defs>
<XAxis
dataKey="time"
stroke="#64748b"
fontSize={12}
tickLine={false}
axisLine={false}
/>
<YAxis
stroke="#64748b"
fontSize={12}
tickLine={false}
axisLine={false}
tickFormatter={(value) => `${value}%`}
domain={[0, 100]}
/>
<Tooltip
contentStyle={{
backgroundColor: '#1e293b',
border: '1px solid #334155',
borderRadius: '8px',
}}
labelStyle={{ color: '#94a3b8' }}
formatter={(value: number) => [`${value}%`, 'Memory']}
/>
<Area
type="monotone"
dataKey="value"
stroke="#8b5cf6"
strokeWidth={2}
fill="url(#memoryGradient)"
/>
</AreaChart>
</ResponsiveContainer>
)
}

View File

@@ -0,0 +1,68 @@
'use client'
import { useState } from 'react'
interface HeaderProps {
onCopilotClick: () => void
}
export default function Header({ onCopilotClick }: HeaderProps) {
const [searchQuery, setSearchQuery] = useState('')
return (
<header className="h-16 bg-slate-900 border-b border-slate-800 flex items-center justify-between px-6">
{/* Search */}
<div className="flex-1 max-w-xl">
<div className="relative">
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-500">
🔍
</span>
<input
type="text"
placeholder="Search hosts, metrics, logs..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-10 pr-4 py-2 bg-slate-800 border border-slate-700 rounded-lg focus:outline-none focus:border-green-500 text-sm"
/>
<span className="absolute right-3 top-1/2 -translate-y-1/2 text-xs text-slate-500 bg-slate-700 px-2 py-0.5 rounded">
K
</span>
</div>
</div>
{/* Right Side */}
<div className="flex items-center space-x-4">
{/* Time Range */}
<select className="bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-green-500">
<option>Last 1 hour</option>
<option>Last 6 hours</option>
<option>Last 24 hours</option>
<option>Last 7 days</option>
<option>Last 30 days</option>
</select>
{/* Refresh */}
<button className="p-2 hover:bg-slate-800 rounded-lg transition" title="Refresh">
🔄
</button>
{/* Notifications */}
<button className="relative p-2 hover:bg-slate-800 rounded-lg transition" title="Notifications">
🔔
<span className="absolute -top-1 -right-1 w-4 h-4 bg-red-500 rounded-full text-xs flex items-center justify-center">
3
</span>
</button>
{/* AI Copilot Button */}
<button
onClick={onCopilotClick}
className="flex items-center space-x-2 px-4 py-2 bg-gradient-to-r from-purple-600 to-purple-700 hover:from-purple-500 hover:to-purple-600 rounded-lg transition font-medium"
>
<span>🤖</span>
<span>AI Copilot</span>
</button>
</div>
</header>
)
}

View File

@@ -0,0 +1,96 @@
'use client'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
const menuItems = [
{ name: 'Overview', icon: '📊', href: '/' },
{ name: 'Hosts', icon: '🖥️', href: '/hosts' },
{ name: 'Containers', icon: '🐳', href: '/containers' },
{ name: 'Metrics', icon: '📈', href: '/metrics' },
{ name: 'Logs', icon: '📝', href: '/logs' },
{ name: 'Traces', icon: '🔍', href: '/traces' },
{ name: 'Alerts', icon: '🚨', href: '/alerts' },
{ name: 'Dashboards', icon: '📋', href: '/dashboards' },
]
const bottomItems = [
{ name: 'AI Insights', icon: '🤖', href: '/ai' },
{ name: 'Settings', icon: '⚙️', href: '/settings' },
]
export default function Sidebar() {
const pathname = usePathname()
return (
<aside className="w-64 bg-slate-900 border-r border-slate-800 flex flex-col">
{/* Logo */}
<div className="p-6 border-b border-slate-800">
<Link href="/" className="flex items-center space-x-3">
<span className="text-3xl">🐍</span>
<div>
<span className="text-xl font-bold bg-gradient-to-r from-green-400 to-emerald-500 bg-clip-text text-transparent">
OPHION
</span>
<span className="ml-2 text-xs bg-purple-600 px-2 py-0.5 rounded-full">AI</span>
</div>
</Link>
</div>
{/* Main Navigation */}
<nav className="flex-1 p-4 space-y-1">
{menuItems.map((item) => {
const isActive = pathname === item.href
return (
<Link
key={item.name}
href={item.href}
className={`flex items-center space-x-3 px-4 py-3 rounded-lg transition-all ${
isActive
? 'bg-green-600/20 text-green-400 border-l-2 border-green-400'
: 'text-slate-400 hover:bg-slate-800 hover:text-white'
}`}
>
<span className="text-xl">{item.icon}</span>
<span className="font-medium">{item.name}</span>
</Link>
)
})}
</nav>
{/* Bottom Navigation */}
<div className="p-4 border-t border-slate-800 space-y-1">
{bottomItems.map((item) => {
const isActive = pathname === item.href
return (
<Link
key={item.name}
href={item.href}
className={`flex items-center space-x-3 px-4 py-3 rounded-lg transition-all ${
isActive
? 'bg-purple-600/20 text-purple-400'
: 'text-slate-400 hover:bg-slate-800 hover:text-white'
}`}
>
<span className="text-xl">{item.icon}</span>
<span className="font-medium">{item.name}</span>
</Link>
)
})}
</div>
{/* User */}
<div className="p-4 border-t border-slate-800">
<div className="flex items-center space-x-3">
<div className="w-10 h-10 rounded-full bg-gradient-to-r from-green-500 to-emerald-600 flex items-center justify-center font-bold">
A
</div>
<div>
<p className="font-medium">Admin</p>
<p className="text-xs text-slate-500">admin@empresa.com</p>
</div>
</div>
</div>
</aside>
)
}

View File

@@ -0,0 +1,78 @@
'use client'
const insights = [
{
type: 'anomaly',
icon: '🔍',
title: 'Anomalia Detectada',
description: 'O servidor cache-01 está com consumo 40% acima do padrão para este horário.',
severity: 'high',
action: 'Investigar',
},
{
type: 'prediction',
icon: '🔮',
title: 'Previsão de Capacidade',
description: 'O disco do servidor db-01 vai atingir 90% em aproximadamente 12 dias.',
severity: 'medium',
action: 'Planejar expansão',
},
{
type: 'optimization',
icon: '💡',
title: 'Oportunidade de Economia',
description: 'O servidor dev-02 está subutilizado (CPU média: 5%). Considere reduzir recursos.',
severity: 'low',
action: 'Ver detalhes',
},
]
export default function AIInsights() {
const getSeverityColor = (severity: string) => {
switch (severity) {
case 'high':
return 'border-red-500/50 bg-red-950/20'
case 'medium':
return 'border-yellow-500/50 bg-yellow-950/20'
default:
return 'border-green-500/50 bg-green-950/20'
}
}
return (
<div className="bg-gradient-to-r from-purple-900/30 to-slate-900 rounded-xl p-6 border border-purple-500/30">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center space-x-3">
<span className="text-2xl">🤖</span>
<div>
<h3 className="text-lg font-semibold">AI Insights</h3>
<p className="text-sm text-slate-400">Análises geradas automaticamente</p>
</div>
</div>
<button className="text-sm text-purple-400 hover:text-purple-300">
Ver todos
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{insights.map((insight, index) => (
<div
key={index}
className={`p-4 rounded-lg border ${getSeverityColor(insight.severity)} hover:bg-slate-800/50 transition cursor-pointer`}
>
<div className="flex items-start space-x-3">
<span className="text-2xl">{insight.icon}</span>
<div className="flex-1">
<p className="font-medium">{insight.title}</p>
<p className="text-sm text-slate-400 mt-1">{insight.description}</p>
<button className="mt-3 text-sm text-purple-400 hover:text-purple-300">
{insight.action}
</button>
</div>
</div>
</div>
))}
</div>
</div>
)
}

View File

@@ -0,0 +1,85 @@
'use client'
const alerts = [
{
id: 1,
severity: 'critical',
title: 'High CPU Usage',
host: 'cache-01',
message: 'CPU at 92% for 10 minutes',
time: '2 min ago',
aiSuggestion: 'Consider restarting Redis or scaling horizontally',
},
{
id: 2,
severity: 'critical',
title: 'Memory Critical',
host: 'cache-01',
message: 'Memory at 94%',
time: '5 min ago',
aiSuggestion: 'Memory leak detected. Recommended: restart service',
},
{
id: 3,
severity: 'warning',
title: 'High Memory Usage',
host: 'api-01',
message: 'Memory at 85%',
time: '15 min ago',
aiSuggestion: 'Monitor for next hour before action',
},
]
export default function AlertsList() {
const getSeverityStyles = (severity: string) => {
switch (severity) {
case 'critical':
return 'border-l-red-500 bg-red-950/30'
case 'warning':
return 'border-l-yellow-500 bg-yellow-950/30'
default:
return 'border-l-blue-500 bg-blue-950/30'
}
}
const getSeverityIcon = (severity: string) => {
switch (severity) {
case 'critical':
return '🔴'
case 'warning':
return '🟡'
default:
return '🔵'
}
}
return (
<div className="space-y-3">
{alerts.map((alert) => (
<div
key={alert.id}
className={`p-4 rounded-lg border-l-4 ${getSeverityStyles(alert.severity)} cursor-pointer hover:bg-slate-800/50 transition`}
>
<div className="flex items-start justify-between">
<div className="flex items-start space-x-3">
<span className="text-xl">{getSeverityIcon(alert.severity)}</span>
<div>
<p className="font-medium">{alert.title}</p>
<p className="text-sm text-slate-400">
{alert.host} {alert.message}
</p>
{alert.aiSuggestion && (
<div className="mt-2 flex items-start space-x-2 text-sm text-purple-400">
<span>🤖</span>
<span>{alert.aiSuggestion}</span>
</div>
)}
</div>
</div>
<span className="text-xs text-slate-500">{alert.time}</span>
</div>
</div>
))}
</div>
)
}

View File

@@ -0,0 +1,251 @@
'use client'
import { useState, useRef, useEffect } from 'react'
interface Message {
role: 'user' | 'assistant'
content: string
actions?: Array<{
type: string
description: string
command?: string
}>
}
interface CopilotProps {
onClose: () => void
}
export default function Copilot({ onClose }: CopilotProps) {
const [messages, setMessages] = useState<Message[]>([
{
role: 'assistant',
content: 'Olá! Sou o OPHION Copilot. Posso ajudar com análise de métricas, troubleshooting, ou qualquer dúvida sobre sua infraestrutura. Como posso ajudar?',
},
])
const [input, setInput] = useState('')
const [isLoading, setIsLoading] = useState(false)
const messagesEndRef = useRef<HTMLDivElement>(null)
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
}
useEffect(() => {
scrollToBottom()
}, [messages])
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!input.trim() || isLoading) return
const userMessage = input.trim()
setInput('')
setMessages((prev) => [...prev, { role: 'user', content: userMessage }])
setIsLoading(true)
// Simulate AI response - replace with real API call
setTimeout(() => {
const response = generateMockResponse(userMessage)
setMessages((prev) => [...prev, response])
setIsLoading(false)
}, 1500)
}
const generateMockResponse = (query: string): Message => {
const lowerQuery = query.toLowerCase()
if (lowerQuery.includes('cpu') || lowerQuery.includes('lento')) {
return {
role: 'assistant',
content: `Analisei as métricas do servidor cache-01 que está com CPU alta (92%).
**Causa provável:** O processo Redis está consumindo mais CPU que o normal, provavelmente devido a um aumento de requisições ou operações de BGSAVE.
**Métricas relacionadas:**
- Conexões ativas: 1,247 (média: 800)
- Operações/s: 45,000 (média: 30,000)
- Memory: 94% (próximo do limite)
**Recomendações:**`,
actions: [
{
type: 'command',
description: 'Reiniciar Redis gracefully',
command: 'redis-cli SHUTDOWN NOSAVE && systemctl start redis',
},
{
type: 'scale',
description: 'Adicionar réplica Redis',
command: 'kubectl scale deployment redis --replicas=2',
},
],
}
}
if (lowerQuery.includes('disco') || lowerQuery.includes('storage')) {
return {
role: 'assistant',
content: `Analisei o uso de disco dos seus servidores:
**Previsões de capacidade:**
- **db-01:** 68% usado, vai atingir 90% em ~12 dias
- **web-01:** 34% usado, estável
- **api-01:** 52% usado, crescendo 2%/dia
**Maiores consumidores em db-01:**
- /var/lib/postgresql: 45GB
- /var/log: 12GB
- /tmp: 3GB
**Sugestão:** Rotacionar logs antigos e configurar retenção.`,
actions: [
{
type: 'command',
description: 'Limpar logs antigos',
command: 'find /var/log -type f -mtime +7 -delete',
},
],
}
}
return {
role: 'assistant',
content: `Entendi sua pergunta sobre "${query}".
Posso ajudar com:
- 📊 Análise de métricas (CPU, memória, disco, rede)
- 🔍 Investigação de problemas
- 📝 Análise de logs
- 🚨 Explicação de alertas
- 💡 Sugestões de otimização
O que você gostaria de saber especificamente?`,
}
}
const quickActions = [
'Por que o cache-01 está lento?',
'Analise os alertas ativos',
'Previsão de disco',
'Resumo das últimas 24h',
]
return (
<div className="w-96 bg-slate-900 border-l border-slate-800 flex flex-col h-full">
{/* Header */}
<div className="p-4 border-b border-slate-800 flex items-center justify-between">
<div className="flex items-center space-x-3">
<span className="text-2xl">🤖</span>
<div>
<h3 className="font-semibold">OPHION Copilot</h3>
<p className="text-xs text-green-400 flex items-center">
<span className="w-2 h-2 bg-green-400 rounded-full mr-1 animate-pulse"></span>
Online
</p>
</div>
</div>
<button
onClick={onClose}
className="p-2 hover:bg-slate-800 rounded-lg transition"
>
</button>
</div>
{/* Messages */}
<div className="flex-1 overflow-y-auto p-4 space-y-4">
{messages.map((message, index) => (
<div
key={index}
className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}
>
<div
className={`max-w-[85%] rounded-lg p-3 ${
message.role === 'user'
? 'bg-green-600 text-white'
: 'bg-slate-800 text-slate-200'
}`}
>
<p className="text-sm whitespace-pre-wrap">{message.content}</p>
{message.actions && message.actions.length > 0 && (
<div className="mt-3 space-y-2">
{message.actions.map((action, actionIndex) => (
<div
key={actionIndex}
className="bg-slate-900/50 rounded p-2 border border-slate-700"
>
<p className="text-xs text-slate-400">{action.description}</p>
{action.command && (
<code className="text-xs text-green-400 bg-slate-950 px-2 py-1 rounded mt-1 block">
{action.command}
</code>
)}
<button className="text-xs text-purple-400 mt-2 hover:text-purple-300">
Executar
</button>
</div>
))}
</div>
)}
</div>
</div>
))}
{isLoading && (
<div className="flex justify-start">
<div className="bg-slate-800 rounded-lg p-3">
<div className="flex space-x-2">
<div className="w-2 h-2 bg-purple-400 rounded-full animate-bounce"></div>
<div className="w-2 h-2 bg-purple-400 rounded-full animate-bounce" style={{ animationDelay: '0.1s' }}></div>
<div className="w-2 h-2 bg-purple-400 rounded-full animate-bounce" style={{ animationDelay: '0.2s' }}></div>
</div>
</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
{/* Quick Actions */}
{messages.length === 1 && (
<div className="px-4 pb-2">
<p className="text-xs text-slate-500 mb-2">Perguntas frequentes:</p>
<div className="flex flex-wrap gap-2">
{quickActions.map((action, index) => (
<button
key={index}
onClick={() => setInput(action)}
className="text-xs bg-slate-800 hover:bg-slate-700 px-3 py-1.5 rounded-full transition"
>
{action}
</button>
))}
</div>
</div>
)}
{/* Input */}
<form onSubmit={handleSubmit} className="p-4 border-t border-slate-800">
<div className="flex space-x-2">
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Pergunte qualquer coisa..."
className="flex-1 bg-slate-800 border border-slate-700 rounded-lg px-4 py-2 text-sm focus:outline-none focus:border-purple-500"
disabled={isLoading}
/>
<button
type="submit"
disabled={isLoading || !input.trim()}
className="px-4 py-2 bg-purple-600 hover:bg-purple-500 disabled:bg-slate-700 disabled:cursor-not-allowed rounded-lg transition"
>
</button>
</div>
</form>
</div>
)
}

View File

@@ -0,0 +1,76 @@
'use client'
const hosts = [
{ name: 'web-01', status: 'healthy', cpu: 45, memory: 62, disk: 34 },
{ name: 'web-02', status: 'healthy', cpu: 38, memory: 58, disk: 41 },
{ name: 'api-01', status: 'warning', cpu: 78, memory: 85, disk: 52 },
{ name: 'db-01', status: 'healthy', cpu: 22, memory: 71, disk: 68 },
{ name: 'cache-01', status: 'critical', cpu: 92, memory: 94, disk: 45 },
]
export default function HostsTable() {
const getStatusColor = (status: string) => {
switch (status) {
case 'healthy':
return 'bg-green-500'
case 'warning':
return 'bg-yellow-500'
case 'critical':
return 'bg-red-500'
default:
return 'bg-slate-500'
}
}
const getMetricColor = (value: number) => {
if (value >= 90) return 'text-red-400'
if (value >= 70) return 'text-yellow-400'
return 'text-green-400'
}
return (
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="text-left text-slate-400 text-sm border-b border-slate-800">
<th className="pb-3">Host</th>
<th className="pb-3">Status</th>
<th className="pb-3">CPU</th>
<th className="pb-3">Memory</th>
<th className="pb-3">Disk</th>
</tr>
</thead>
<tbody>
{hosts.map((host) => (
<tr
key={host.name}
className="border-b border-slate-800/50 hover:bg-slate-800/30 transition cursor-pointer"
>
<td className="py-3">
<div className="flex items-center space-x-2">
<span>🖥</span>
<span className="font-medium">{host.name}</span>
</div>
</td>
<td className="py-3">
<div className="flex items-center space-x-2">
<span className={`w-2 h-2 rounded-full ${getStatusColor(host.status)}`}></span>
<span className="capitalize text-sm">{host.status}</span>
</div>
</td>
<td className={`py-3 ${getMetricColor(host.cpu)}`}>
{host.cpu}%
</td>
<td className={`py-3 ${getMetricColor(host.memory)}`}>
{host.memory}%
</td>
<td className={`py-3 ${getMetricColor(host.disk)}`}>
{host.disk}%
</td>
</tr>
))}
</tbody>
</table>
</div>
)
}

View File

@@ -0,0 +1,57 @@
'use client'
interface MetricCardProps {
title: string
value: string | number
subtitle?: string
icon: string
trend?: 'up' | 'down' | 'stable'
alert?: boolean
}
export default function MetricCard({ title, value, subtitle, icon, trend, alert }: MetricCardProps) {
const getTrendIcon = () => {
switch (trend) {
case 'up':
return <span className="text-red-400"></span>
case 'down':
return <span className="text-green-400"></span>
default:
return <span className="text-slate-400"></span>
}
}
return (
<div
className={`bg-slate-900 rounded-xl p-6 border transition-all card-hover ${
alert ? 'border-red-500/50 bg-red-950/20' : 'border-slate-800'
}`}
>
<div className="flex items-start justify-between">
<div>
<p className="text-slate-400 text-sm mb-1">{title}</p>
<p className="text-3xl font-bold">{value}</p>
{subtitle && (
<p className="text-slate-500 text-sm mt-1 flex items-center space-x-1">
{getTrendIcon()}
<span>{subtitle}</span>
</p>
)}
</div>
<div
className={`text-3xl p-3 rounded-lg ${
alert ? 'bg-red-500/20' : 'bg-slate-800'
}`}
>
{icon}
</div>
</div>
{alert && (
<div className="mt-4 flex items-center space-x-2 text-red-400 text-sm">
<span className="w-2 h-2 bg-red-400 rounded-full animate-pulse"></span>
<span>Needs attention</span>
</div>
)}
</div>
)
}

View File

@@ -0,0 +1,13 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: 'class',
content: [
'./src/pages/**/*.{js,ts,jsx,tsx,mdx}',
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',
'./src/app/**/*.{js,ts,jsx,tsx,mdx}',
],
theme: {
extend: {},
},
plugins: [],
}

20
dashboard/tsconfig.json Normal file
View File

@@ -0,0 +1,20 @@
{
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [{ "name": "next" }],
"paths": { "@/*": ["./src/*"] }
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}