From 011a93c5b90632029d5bce5118b336d32d8ec29c Mon Sep 17 00:00:00 2001 From: Ricel Leite Date: Wed, 18 Feb 2026 14:02:13 -0300 Subject: [PATCH] feat: Portal Web React + TailwindCSS - Dashboard with stats cards and quick actions - Issues list with filters and status badges - Repositories management with add/reindex/delete - Modules editor for business rules - Settings page for integrations (JIRA, Bitbucket, LLM) - Responsive sidebar navigation - React Query for data fetching Pages: Dashboard, Issues, Repositories, Modules, Settings --- portal/Dockerfile | 12 ++ portal/index.html | 13 ++ portal/package.json | 30 +++ portal/postcss.config.js | 6 + portal/src/App.tsx | 23 +++ portal/src/components/Layout.tsx | 84 ++++++++ portal/src/index.css | 7 + portal/src/main.tsx | 25 +++ portal/src/pages/Dashboard.tsx | 125 ++++++++++++ portal/src/pages/Issues.tsx | 190 ++++++++++++++++++ portal/src/pages/Modules.tsx | 291 +++++++++++++++++++++++++++ portal/src/pages/Repositories.tsx | 233 ++++++++++++++++++++++ portal/src/pages/Settings.tsx | 319 ++++++++++++++++++++++++++++++ portal/tailwind.config.js | 26 +++ portal/tsconfig.json | 25 +++ portal/tsconfig.node.json | 10 + portal/vite.config.ts | 15 ++ 17 files changed, 1434 insertions(+) create mode 100644 portal/Dockerfile create mode 100644 portal/index.html create mode 100644 portal/package.json create mode 100644 portal/postcss.config.js create mode 100644 portal/src/App.tsx create mode 100644 portal/src/components/Layout.tsx create mode 100644 portal/src/index.css create mode 100644 portal/src/main.tsx create mode 100644 portal/src/pages/Dashboard.tsx create mode 100644 portal/src/pages/Issues.tsx create mode 100644 portal/src/pages/Modules.tsx create mode 100644 portal/src/pages/Repositories.tsx create mode 100644 portal/src/pages/Settings.tsx create mode 100644 portal/tailwind.config.js create mode 100644 portal/tsconfig.json create mode 100644 portal/tsconfig.node.json create mode 100644 portal/vite.config.ts diff --git a/portal/Dockerfile b/portal/Dockerfile new file mode 100644 index 0000000..e977b85 --- /dev/null +++ b/portal/Dockerfile @@ -0,0 +1,12 @@ +FROM node:20-alpine + +WORKDIR /app + +COPY package*.json ./ +RUN npm install + +COPY . . + +EXPOSE 3000 + +CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"] diff --git a/portal/index.html b/portal/index.html new file mode 100644 index 0000000..ae8ee73 --- /dev/null +++ b/portal/index.html @@ -0,0 +1,13 @@ + + + + + + + ACI AI Fixer - Portal + + +
+ + + diff --git a/portal/package.json b/portal/package.json new file mode 100644 index 0000000..617229d --- /dev/null +++ b/portal/package.json @@ -0,0 +1,30 @@ +{ + "name": "aci-ai-fixer-portal", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.21.0", + "@tanstack/react-query": "^5.17.0", + "axios": "^1.6.0", + "lucide-react": "^0.303.0", + "clsx": "^2.1.0" + }, + "devDependencies": { + "@types/react": "^18.2.0", + "@types/react-dom": "^18.2.0", + "@vitejs/plugin-react": "^4.2.0", + "autoprefixer": "^10.4.0", + "postcss": "^8.4.0", + "tailwindcss": "^3.4.0", + "typescript": "^5.3.0", + "vite": "^5.0.0" + } +} diff --git a/portal/postcss.config.js b/portal/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/portal/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/portal/src/App.tsx b/portal/src/App.tsx new file mode 100644 index 0000000..225fc2c --- /dev/null +++ b/portal/src/App.tsx @@ -0,0 +1,23 @@ +import { Routes, Route } from 'react-router-dom' +import Layout from './components/Layout' +import Dashboard from './pages/Dashboard' +import Issues from './pages/Issues' +import Repositories from './pages/Repositories' +import Modules from './pages/Modules' +import Settings from './pages/Settings' + +function App() { + return ( + + + } /> + } /> + } /> + } /> + } /> + + + ) +} + +export default App diff --git a/portal/src/components/Layout.tsx b/portal/src/components/Layout.tsx new file mode 100644 index 0000000..efa9910 --- /dev/null +++ b/portal/src/components/Layout.tsx @@ -0,0 +1,84 @@ +import { Link, useLocation } from 'react-router-dom' +import { + LayoutDashboard, + AlertCircle, + GitBranch, + Brain, + Settings, + Cpu +} from 'lucide-react' +import { clsx } from 'clsx' + +const navigation = [ + { name: 'Dashboard', href: '/', icon: LayoutDashboard }, + { name: 'Issues', href: '/issues', icon: AlertCircle }, + { name: 'Repositórios', href: '/repositories', icon: GitBranch }, + { name: 'Módulos', href: '/modules', icon: Brain }, + { name: 'Configurações', href: '/settings', icon: Settings }, +] + +interface LayoutProps { + children: React.ReactNode +} + +export default function Layout({ children }: LayoutProps) { + const location = useLocation() + + return ( +
+ {/* Sidebar */} +
+
+ +
+

ACI AI Fixer

+

v0.1.0

+
+
+ + +
+ + {/* Main content */} +
+ {/* Header */} +
+
+

+ {navigation.find(n => n.href === location.pathname)?.name || 'ACI AI Fixer'} +

+
+ + Ambiente: Desenvolvimento + +
+
+
+ + {/* Page content */} +
+ {children} +
+
+
+ ) +} diff --git a/portal/src/index.css b/portal/src/index.css new file mode 100644 index 0000000..c2f490f --- /dev/null +++ b/portal/src/index.css @@ -0,0 +1,7 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +body { + @apply bg-gray-50 text-gray-900; +} diff --git a/portal/src/main.tsx b/portal/src/main.tsx new file mode 100644 index 0000000..f630e51 --- /dev/null +++ b/portal/src/main.tsx @@ -0,0 +1,25 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { BrowserRouter } from 'react-router-dom' +import App from './App' +import './index.css' + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 1000 * 60 * 5, // 5 minutes + retry: 1, + }, + }, +}) + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + + + + + , +) diff --git a/portal/src/pages/Dashboard.tsx b/portal/src/pages/Dashboard.tsx new file mode 100644 index 0000000..7c5202a --- /dev/null +++ b/portal/src/pages/Dashboard.tsx @@ -0,0 +1,125 @@ +import { useQuery } from '@tanstack/react-query' +import { + CheckCircle, + Clock, + AlertTriangle, + XCircle, + TrendingUp, + Activity +} from 'lucide-react' +import { api } from '../lib/api' + +interface StatsCardProps { + title: string + value: string | number + icon: React.ReactNode + trend?: string + color: string +} + +function StatsCard({ title, value, icon, trend, color }: StatsCardProps) { + return ( +
+
+
+

{title}

+

{value}

+ {trend && ( +

+ + {trend} +

+ )} +
+
+ {icon} +
+
+
+ ) +} + +export default function Dashboard() { + const { data: stats, isLoading } = useQuery({ + queryKey: ['stats'], + queryFn: () => api.get('/api/issues/stats/summary').then(r => r.data), + }) + + if (isLoading) { + return ( +
+ +
+ ) + } + + return ( +
+ {/* Stats Grid */} +
+ } + color="bg-blue-500" + /> + } + color="bg-yellow-500" + /> + } + color="bg-green-500" + trend={stats?.success_rate ? `${(stats.success_rate * 100).toFixed(0)}% sucesso` : undefined} + /> + } + color="bg-red-500" + /> +
+ + {/* Recent Activity */} +
+
+

Atividade Recente

+
+
+
+ +

Nenhuma atividade recente

+

+ Configure os webhooks do JIRA para começar a receber issues +

+
+
+
+ + {/* Quick Actions */} +
+
+

Ações Rápidas

+
+
+ + + +
+
+
+ ) +} diff --git a/portal/src/pages/Issues.tsx b/portal/src/pages/Issues.tsx new file mode 100644 index 0000000..225f09d --- /dev/null +++ b/portal/src/pages/Issues.tsx @@ -0,0 +1,190 @@ +import { useQuery } from '@tanstack/react-query' +import { useState } from 'react' +import { + Search, + Filter, + RefreshCw, + ExternalLink, + CheckCircle, + Clock, + XCircle, + Activity +} from 'lucide-react' +import { api } from '../lib/api' +import { clsx } from 'clsx' + +type IssueStatus = 'pending' | 'analyzing' | 'analyzed' | 'fix_generated' | 'pr_created' | 'accepted' | 'rejected' | 'failed' + +interface Issue { + id: string + jira_key: string + title: string + status: IssueStatus + module?: string + confidence?: number + created_at: string + pr_url?: string +} + +const statusConfig: Record = { + pending: { label: 'Pendente', color: 'bg-gray-100 text-gray-800', icon: }, + analyzing: { label: 'Analisando', color: 'bg-blue-100 text-blue-800', icon: }, + analyzed: { label: 'Analisado', color: 'bg-purple-100 text-purple-800', icon: }, + fix_generated: { label: 'Fix Gerado', color: 'bg-indigo-100 text-indigo-800', icon: }, + pr_created: { label: 'PR Criado', color: 'bg-cyan-100 text-cyan-800', icon: }, + accepted: { label: 'Aceito', color: 'bg-green-100 text-green-800', icon: }, + rejected: { label: 'Rejeitado', color: 'bg-red-100 text-red-800', icon: }, + failed: { label: 'Falhou', color: 'bg-red-100 text-red-800', icon: }, +} + +export default function Issues() { + const [search, setSearch] = useState('') + const [statusFilter, setStatusFilter] = useState('') + + const { data, isLoading, refetch } = useQuery({ + queryKey: ['issues', statusFilter], + queryFn: () => api.get('/api/issues', { + params: { status: statusFilter || undefined } + }).then(r => r.data), + }) + + const issues: Issue[] = data?.items || [] + + const filteredIssues = issues.filter(issue => + issue.jira_key.toLowerCase().includes(search.toLowerCase()) || + issue.title.toLowerCase().includes(search.toLowerCase()) + ) + + return ( +
+ {/* Filters */} +
+
+
+ + setSearch(e.target.value)} + className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" + /> +
+ +
+ + +
+ + +
+
+ + {/* Issues Table */} +
+ {isLoading ? ( +
+ +
+ ) : filteredIssues.length === 0 ? ( +
+ +

Nenhuma issue encontrada

+

+ Issues do JIRA aparecerão aqui após configurar os webhooks +

+
+ ) : ( + + + + + + + + + + + + + {filteredIssues.map(issue => { + const status = statusConfig[issue.status] + return ( + + + + + + + + + ) + })} + +
JIRA KeyTítuloMóduloStatusConfiançaAções
+ + {issue.jira_key} + + + {issue.title} + + {issue.module || '-'} + + + {status.icon} + {status.label} + + + {issue.confidence ? ( +
+
+
+
+ + {(issue.confidence * 100).toFixed(0)}% + +
+ ) : '-'} +
+
+ + {issue.pr_url && ( + + PR + + )} +
+
+ )} +
+
+ ) +} diff --git a/portal/src/pages/Modules.tsx b/portal/src/pages/Modules.tsx new file mode 100644 index 0000000..b4e402b --- /dev/null +++ b/portal/src/pages/Modules.tsx @@ -0,0 +1,291 @@ +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' +import { useState } from 'react' +import { + Plus, + Edit2, + Trash2, + Brain, + Save, + X, + Activity, + BookOpen +} from 'lucide-react' +import { api } from '../lib/api' + +interface Module { + name: string + description?: string + program_patterns: string[] + keywords: string[] + rules: string[] + restrictions: string[] +} + +export default function Modules() { + const queryClient = useQueryClient() + const [editingModule, setEditingModule] = useState(null) + const [isNew, setIsNew] = useState(false) + + const { data: modules, isLoading } = useQuery({ + queryKey: ['modules'], + queryFn: () => api.get('/api/config/modules').then(r => r.data), + }) + + const saveMutation = useMutation({ + mutationFn: (module: Module) => + isNew + ? api.post('/api/config/modules', module) + : api.put(`/api/config/modules/${module.name}`, module), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['modules'] }) + setEditingModule(null) + setIsNew(false) + }, + }) + + const deleteMutation = useMutation({ + mutationFn: (name: string) => api.delete(`/api/config/modules/${name}`), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['modules'] }) + }, + }) + + const modulesList: Module[] = modules || [] + + const handleNewModule = () => { + setEditingModule({ + name: '', + description: '', + program_patterns: [], + keywords: [], + rules: [], + restrictions: [], + }) + setIsNew(true) + } + + return ( +
+ {/* Header */} +
+
+

+ Módulos de regras de negócio para contextualização da IA +

+
+ +
+ + {/* Modules List */} + {isLoading ? ( +
+ +
+ ) : modulesList.length === 0 && !editingModule ? ( +
+ +

Nenhum módulo configurado

+

+ Módulos definem regras de negócio específicas que a IA usa para entender o contexto do sistema +

+ +
+ ) : ( +
+ {modulesList.map(module => ( +
+
+
+ +
+

{module.name}

+

{module.description || 'Sem descrição'}

+
+
+
+ + +
+
+ +
+
+ Padrões de programa: +
+ {module.program_patterns.map(p => ( + + {p} + + ))} + {module.program_patterns.length === 0 && -} +
+
+
+ Keywords: +
+ {module.keywords.slice(0, 5).map(k => ( + + {k} + + ))} + {module.keywords.length > 5 && ( + +{module.keywords.length - 5} + )} + {module.keywords.length === 0 && -} +
+
+
+
+ ))} +
+ )} + + {/* Edit/Create Modal */} + {editingModule && ( +
+
+
+

+ {isNew ? 'Novo Módulo' : `Editar: ${editingModule.name}`} +

+ +
+ +
+
+ + setEditingModule({ ...editingModule, name: e.target.value })} + disabled={!isNew} + placeholder="ACQ-Auth" + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100" + /> +
+ +
+ +