diff --git a/dashboard/next.config.js b/dashboard/next.config.js new file mode 100644 index 0000000..5cd8cc3 --- /dev/null +++ b/dashboard/next.config.js @@ -0,0 +1,6 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + output: 'standalone', +} + +module.exports = nextConfig diff --git a/dashboard/package.json b/dashboard/package.json new file mode 100644 index 0000000..7493da4 --- /dev/null +++ b/dashboard/package.json @@ -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" + } +} diff --git a/dashboard/postcss.config.js b/dashboard/postcss.config.js new file mode 100644 index 0000000..33ad091 --- /dev/null +++ b/dashboard/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/dashboard/src/app/globals.css b/dashboard/src/app/globals.css new file mode 100644 index 0000000..03679e3 --- /dev/null +++ b/dashboard/src/app/globals.css @@ -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); +} diff --git a/dashboard/src/app/layout.tsx b/dashboard/src/app/layout.tsx new file mode 100644 index 0000000..02a8fcd --- /dev/null +++ b/dashboard/src/app/layout.tsx @@ -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 ( + + + {children} + + + ) +} diff --git a/dashboard/src/app/page.tsx b/dashboard/src/app/page.tsx new file mode 100644 index 0000000..5a4aaa0 --- /dev/null +++ b/dashboard/src/app/page.tsx @@ -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 ( +
+ {/* Sidebar */} + + + {/* Main Content */} +
+ {/* Header */} +
setShowCopilot(true)} /> + + {/* Dashboard Content */} +
+ {/* Top Metrics */} +
+ + 70 ? 'up' : 'stable'} + alert={metrics.cpuAvg > 80} + /> + 70 ? 'up' : 'stable'} + alert={metrics.memoryAvg > 85} + /> + 0 ? 'up' : 'down'} + alert={metrics.activeAlerts > 0} + /> +
+ + {/* Charts Row */} +
+
+

CPU Usage (24h)

+ +
+
+

Memory Usage (24h)

+ +
+
+ + {/* AI Insights */} +
+ +
+ + {/* Bottom Section */} +
+ {/* Hosts Table */} +
+
+

Hosts

+ +
+ +
+ + {/* Alerts */} +
+
+

Recent Alerts

+ +
+ +
+
+
+
+ + {/* AI Copilot Sidebar */} + {showCopilot && ( + setShowCopilot(false)} /> + )} +
+ ) +} diff --git a/dashboard/src/components/charts/CpuChart.tsx b/dashboard/src/components/charts/CpuChart.tsx new file mode 100644 index 0000000..2e6eab7 --- /dev/null +++ b/dashboard/src/components/charts/CpuChart.tsx @@ -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 ( + + + + + + + + + + `${value}%`} + domain={[0, 100]} + /> + [`${value}%`, 'CPU']} + /> + + + + ) +} diff --git a/dashboard/src/components/charts/MemoryChart.tsx b/dashboard/src/components/charts/MemoryChart.tsx new file mode 100644 index 0000000..5c7d629 --- /dev/null +++ b/dashboard/src/components/charts/MemoryChart.tsx @@ -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 ( + + + + + + + + + + `${value}%`} + domain={[0, 100]} + /> + [`${value}%`, 'Memory']} + /> + + + + ) +} diff --git a/dashboard/src/components/layout/Header.tsx b/dashboard/src/components/layout/Header.tsx new file mode 100644 index 0000000..1c345ab --- /dev/null +++ b/dashboard/src/components/layout/Header.tsx @@ -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 ( +
+ {/* Search */} +
+
+ + 🔍 + + 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" + /> + + ⌘K + +
+
+ + {/* Right Side */} +
+ {/* Time Range */} + + + {/* Refresh */} + + + {/* Notifications */} + + + {/* AI Copilot Button */} + +
+
+ ) +} diff --git a/dashboard/src/components/layout/Sidebar.tsx b/dashboard/src/components/layout/Sidebar.tsx new file mode 100644 index 0000000..3e9fd62 --- /dev/null +++ b/dashboard/src/components/layout/Sidebar.tsx @@ -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 ( + + ) +} diff --git a/dashboard/src/components/ui/AIInsights.tsx b/dashboard/src/components/ui/AIInsights.tsx new file mode 100644 index 0000000..2b77e9f --- /dev/null +++ b/dashboard/src/components/ui/AIInsights.tsx @@ -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 ( +
+
+
+ 🤖 +
+

AI Insights

+

Análises geradas automaticamente

+
+
+ +
+ +
+ {insights.map((insight, index) => ( +
+
+ {insight.icon} +
+

{insight.title}

+

{insight.description}

+ +
+
+
+ ))} +
+
+ ) +} diff --git a/dashboard/src/components/ui/AlertsList.tsx b/dashboard/src/components/ui/AlertsList.tsx new file mode 100644 index 0000000..c9769fb --- /dev/null +++ b/dashboard/src/components/ui/AlertsList.tsx @@ -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 ( +
+ {alerts.map((alert) => ( +
+
+
+ {getSeverityIcon(alert.severity)} +
+

{alert.title}

+

+ {alert.host} • {alert.message} +

+ {alert.aiSuggestion && ( +
+ 🤖 + {alert.aiSuggestion} +
+ )} +
+
+ {alert.time} +
+
+ ))} +
+ ) +} diff --git a/dashboard/src/components/ui/Copilot.tsx b/dashboard/src/components/ui/Copilot.tsx new file mode 100644 index 0000000..e0c526c --- /dev/null +++ b/dashboard/src/components/ui/Copilot.tsx @@ -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([ + { + 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(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 ( +
+ {/* Header */} +
+
+ 🤖 +
+

OPHION Copilot

+

+ + Online +

+
+
+ +
+ + {/* Messages */} +
+ {messages.map((message, index) => ( +
+
+

{message.content}

+ + {message.actions && message.actions.length > 0 && ( +
+ {message.actions.map((action, actionIndex) => ( +
+

{action.description}

+ {action.command && ( + + {action.command} + + )} + +
+ ))} +
+ )} +
+
+ ))} + + {isLoading && ( +
+
+
+
+
+
+
+
+
+ )} + +
+
+ + {/* Quick Actions */} + {messages.length === 1 && ( +
+

Perguntas frequentes:

+
+ {quickActions.map((action, index) => ( + + ))} +
+
+ )} + + {/* Input */} +
+
+ 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} + /> + +
+
+
+ ) +} diff --git a/dashboard/src/components/ui/HostsTable.tsx b/dashboard/src/components/ui/HostsTable.tsx new file mode 100644 index 0000000..292139d --- /dev/null +++ b/dashboard/src/components/ui/HostsTable.tsx @@ -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 ( +
+ + + + + + + + + + + + {hosts.map((host) => ( + + + + + + + + ))} + +
HostStatusCPUMemoryDisk
+
+ 🖥️ + {host.name} +
+
+
+ + {host.status} +
+
+ {host.cpu}% + + {host.memory}% + + {host.disk}% +
+
+ ) +} diff --git a/dashboard/src/components/ui/MetricCard.tsx b/dashboard/src/components/ui/MetricCard.tsx new file mode 100644 index 0000000..929d7fc --- /dev/null +++ b/dashboard/src/components/ui/MetricCard.tsx @@ -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 + case 'down': + return + default: + return + } + } + + return ( +
+
+
+

{title}

+

{value}

+ {subtitle && ( +

+ {getTrendIcon()} + {subtitle} +

+ )} +
+
+ {icon} +
+
+ {alert && ( +
+ + Needs attention +
+ )} +
+ ) +} diff --git a/dashboard/tailwind.config.js b/dashboard/tailwind.config.js new file mode 100644 index 0000000..24b939f --- /dev/null +++ b/dashboard/tailwind.config.js @@ -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: [], +} diff --git a/dashboard/tsconfig.json b/dashboard/tsconfig.json new file mode 100644 index 0000000..49e4cf3 --- /dev/null +++ b/dashboard/tsconfig.json @@ -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"] +}