Initial commit: JIRA AI Fixer Portal

- React 18 + TypeScript + Vite
- TailwindCSS styling
- React Query for data fetching
- React Router for navigation

Pages:
- Dashboard with stats
- Issues list with filters
- Issue detail view
- Repositories management
- Settings/Integrations
This commit is contained in:
Ricel Leite 2026-02-18 18:26:26 -03:00
parent 0b24d51ce1
commit fd966983a3
20 changed files with 938 additions and 2 deletions

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
node_modules
dist
.env
.env.local
*.log
.DS_Store

View File

@ -1,3 +1,55 @@
# jira-ai-fixer-portal
# JIRA AI Fixer Portal
JIRA AI Fixer Portal - React Dashboard for intelligent support case resolution
React dashboard for the JIRA AI Fixer - Intelligent Support Case Resolution system.
## Features
- 📊 **Dashboard** - Real-time stats and recent issues
- 🎫 **Issues** - Browse and filter analyzed issues
- 📁 **Repositories** - Manage connected code repositories
- ⚙️ **Settings** - Configure integrations and AI settings
## Tech Stack
- React 18 + TypeScript
- Vite
- TailwindCSS
- React Query
- React Router
## Development
```bash
# Install dependencies
npm install
# Start dev server
npm run dev
# Build for production
npm run build
```
## Environment Variables
```env
VITE_API_URL=https://jira-fixer.startdata.com.br/api
```
## Integrations
### Issue Trackers
- TicketHub (Active)
- JIRA (Ready)
- ServiceNow (Ready)
- Azure DevOps (Ready)
### Code Repositories
- Gitea (Active)
- GitHub (Ready)
- GitLab (Ready)
- Bitbucket (Ready)
## License
MIT

13
index.html Normal file
View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>JIRA AI Fixer Portal</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

31
package.json Normal file
View File

@ -0,0 +1,31 @@
{
"name": "jira-ai-fixer-portal",
"version": "1.0.0",
"description": "JIRA AI Fixer Portal - React Dashboard",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.22.0",
"@tanstack/react-query": "^5.17.0",
"axios": "^1.6.0",
"date-fns": "^3.3.0",
"recharts": "^2.12.0"
},
"devDependencies": {
"@types/react": "^18.2.48",
"@types/react-dom": "^18.2.18",
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.17",
"postcss": "^8.4.33",
"tailwindcss": "^3.4.1",
"typescript": "^5.3.3",
"vite": "^5.0.12"
}
}

6
postcss.config.js Normal file
View File

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

3
public/favicon.svg Normal file
View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<text y=".9em" font-size="90">🤖</text>
</svg>

After

Width:  |  Height:  |  Size: 114 B

21
src/App.tsx Normal file
View File

@ -0,0 +1,21 @@
import { Routes, Route } from 'react-router-dom'
import Layout from './components/Layout'
import Dashboard from './pages/Dashboard'
import Issues from './pages/Issues'
import IssueDetail from './pages/IssueDetail'
import Repositories from './pages/Repositories'
import Settings from './pages/Settings'
export default function App() {
return (
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Dashboard />} />
<Route path="issues" element={<Issues />} />
<Route path="issues/:id" element={<IssueDetail />} />
<Route path="repositories" element={<Repositories />} />
<Route path="settings" element={<Settings />} />
</Route>
</Routes>
)
}

60
src/components/Layout.tsx Normal file
View File

@ -0,0 +1,60 @@
import { Outlet, NavLink } from 'react-router-dom'
const navItems = [
{ to: '/', label: 'Dashboard', icon: '📊' },
{ to: '/issues', label: 'Issues', icon: '🎫' },
{ to: '/repositories', label: 'Repositories', icon: '📁' },
{ to: '/settings', label: 'Settings', icon: '⚙️' },
]
export default function Layout() {
return (
<div className="min-h-screen bg-gray-900 text-white flex">
{/* Sidebar */}
<aside className="w-64 bg-gray-800 border-r border-gray-700 flex flex-col">
<div className="p-4 border-b border-gray-700">
<div className="flex items-center gap-2">
<span className="text-2xl">🤖</span>
<div>
<h1 className="font-bold">JIRA AI Fixer</h1>
<p className="text-xs text-gray-400">Portal</p>
</div>
</div>
</div>
<nav className="flex-1 p-4">
<ul className="space-y-1">
{navItems.map(item => (
<li key={item.to}>
<NavLink
to={item.to}
end={item.to === '/'}
className={({ isActive }) =>
`flex items-center gap-3 px-3 py-2 rounded-lg transition-colors ${
isActive
? 'bg-blue-600 text-white'
: 'text-gray-400 hover:bg-gray-700 hover:text-white'
}`
}
>
<span>{item.icon}</span>
<span>{item.label}</span>
</NavLink>
</li>
))}
</ul>
</nav>
<div className="p-4 border-t border-gray-700 text-xs text-gray-500">
<p>JIRA AI Fixer v1.0.0</p>
<p className="mt-1">© 2026 StartData</p>
</div>
</aside>
{/* Main content */}
<main className="flex-1 overflow-auto">
<Outlet />
</main>
</div>
)
}

7
src/index.css Normal file
View File

@ -0,0 +1,7 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
font-family: 'Inter', system-ui, sans-serif;
}

25
src/main.tsx Normal file
View File

@ -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: {
refetchOnWindowFocus: false,
retry: 1,
},
},
})
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<App />
</BrowserRouter>
</QueryClientProvider>
</React.StrictMode>,
)

137
src/pages/Dashboard.tsx Normal file
View File

@ -0,0 +1,137 @@
import { useQuery } from '@tanstack/react-query'
import { issuesApi } from '../services/api'
import { Link } from 'react-router-dom'
export default function Dashboard() {
const { data: stats } = useQuery({
queryKey: ['stats'],
queryFn: issuesApi.getStats,
refetchInterval: 10000,
})
const { data: issues } = useQuery({
queryKey: ['issues'],
queryFn: () => issuesApi.list(),
refetchInterval: 10000,
})
const recentIssues = issues?.slice(0, 5) || []
return (
<div className="p-6">
<h1 className="text-2xl font-bold mb-6">Dashboard</h1>
{/* Stats Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
<StatCard
title="Total Issues"
value={stats?.total || 0}
icon="📋"
color="blue"
/>
<StatCard
title="Analyzed"
value={stats?.analyzed || 0}
icon="✅"
color="green"
/>
<StatCard
title="PRs Created"
value={stats?.prs_created || 0}
icon="🔀"
color="purple"
/>
<StatCard
title="Avg Confidence"
value={`${stats?.avg_confidence || 0}%`}
icon="🎯"
color="yellow"
/>
</div>
{/* Recent Issues */}
<div className="bg-gray-800 rounded-xl border border-gray-700">
<div className="p-4 border-b border-gray-700 flex justify-between items-center">
<h2 className="font-semibold">Recent Issues</h2>
<Link to="/issues" className="text-sm text-blue-400 hover:text-blue-300">
View all
</Link>
</div>
<div className="divide-y divide-gray-700">
{recentIssues.length === 0 ? (
<div className="p-8 text-center text-gray-500">No issues yet</div>
) : (
recentIssues.map(issue => (
<Link
key={issue.id}
to={`/issues/${issue.id}`}
className="p-4 block hover:bg-gray-700/50 transition-colors"
>
<div className="flex justify-between items-start">
<div>
<div className="flex items-center gap-2">
<span className="font-mono text-blue-400 text-sm">
{issue.external_key || `#${issue.id}`}
</span>
<StatusBadge status={issue.status} />
</div>
<h3 className="font-medium mt-1">{issue.title}</h3>
</div>
{issue.confidence && (
<span className="text-sm text-green-400">
{Math.round(issue.confidence * 100)}%
</span>
)}
</div>
</Link>
))
)}
</div>
</div>
</div>
)
}
function StatCard({ title, value, icon, color }: {
title: string
value: number | string
icon: string
color: 'blue' | 'green' | 'purple' | 'yellow'
}) {
const colors = {
blue: 'bg-blue-500/20 text-blue-400',
green: 'bg-green-500/20 text-green-400',
purple: 'bg-purple-500/20 text-purple-400',
yellow: 'bg-yellow-500/20 text-yellow-400',
}
return (
<div className="bg-gray-800 rounded-xl p-6 border border-gray-700">
<div className="flex items-center justify-between">
<div>
<p className="text-gray-400 text-sm">{title}</p>
<p className={`text-3xl font-bold mt-1 ${colors[color].split(' ')[1]}`}>
{value}
</p>
</div>
<div className={`w-12 h-12 rounded-lg flex items-center justify-center ${colors[color].split(' ')[0]}`}>
<span className="text-2xl">{icon}</span>
</div>
</div>
</div>
)
}
function StatusBadge({ status }: { status: string }) {
const styles = {
analyzed: 'bg-green-500/20 text-green-400',
pending: 'bg-yellow-500/20 text-yellow-400',
error: 'bg-red-500/20 text-red-400',
}
return (
<span className={`px-2 py-0.5 rounded text-xs ${styles[status as keyof typeof styles] || 'bg-gray-500/20 text-gray-400'}`}>
{status}
</span>
)
}

139
src/pages/IssueDetail.tsx Normal file
View File

@ -0,0 +1,139 @@
import { useParams, Link } from 'react-router-dom'
import { useQuery } from '@tanstack/react-query'
import { issuesApi } from '../services/api'
export default function IssueDetail() {
const { id } = useParams()
const { data: issue, isLoading, error } = useQuery({
queryKey: ['issue', id],
queryFn: () => issuesApi.get(Number(id)),
enabled: !!id,
})
if (isLoading) {
return (
<div className="p-6">
<div className="text-center text-gray-500">Loading...</div>
</div>
)
}
if (error || !issue) {
return (
<div className="p-6">
<div className="text-center text-red-400">Issue not found</div>
<Link to="/issues" className="block text-center text-blue-400 mt-4">
Back to issues
</Link>
</div>
)
}
const statusStyles = {
analyzed: 'bg-green-500/20 text-green-400 border-green-500/30',
pending: 'bg-yellow-500/20 text-yellow-400 border-yellow-500/30',
error: 'bg-red-500/20 text-red-400 border-red-500/30',
}
let affectedFiles: string[] = []
try {
affectedFiles = JSON.parse(issue.affected_files || '[]')
} catch (e) {}
return (
<div className="p-6">
{/* Header */}
<div className="mb-6">
<Link to="/issues" className="text-blue-400 hover:text-blue-300 text-sm">
Back to issues
</Link>
</div>
<div className="bg-gray-800 rounded-xl border border-gray-700 p-6 mb-6">
<div className="flex items-start justify-between">
<div>
<div className="flex items-center gap-3">
<span className="font-mono text-blue-400 text-lg">
{issue.external_key || `#${issue.id}`}
</span>
<span className={`px-2 py-1 rounded text-sm border ${statusStyles[issue.status] || ''}`}>
{issue.status}
</span>
</div>
<h1 className="text-2xl font-bold mt-2">{issue.title}</h1>
<p className="text-gray-400 mt-1">Source: {issue.source}</p>
</div>
{issue.confidence && (
<div className="text-right">
<p className="text-sm text-gray-400">Confidence</p>
<p className="text-3xl font-bold text-green-400">
{Math.round(issue.confidence * 100)}%
</p>
</div>
)}
</div>
</div>
{/* Description */}
<div className="bg-gray-800 rounded-xl border border-gray-700 p-6 mb-6">
<h2 className="font-semibold mb-3">Description</h2>
<pre className="whitespace-pre-wrap text-gray-300 bg-gray-900 p-4 rounded-lg">
{issue.description}
</pre>
</div>
{/* Analysis */}
{issue.analysis && (
<div className="bg-green-500/10 border border-green-500/30 rounded-xl p-6 mb-6">
<h2 className="font-semibold text-green-400 mb-3">🔍 Analysis</h2>
<pre className="whitespace-pre-wrap text-gray-300">
{issue.analysis}
</pre>
</div>
)}
{/* Affected Files */}
{affectedFiles.length > 0 && (
<div className="bg-gray-800 rounded-xl border border-gray-700 p-6 mb-6">
<h2 className="font-semibold mb-3">📁 Affected Files</h2>
<div className="flex flex-wrap gap-2">
{affectedFiles.map((file, i) => (
<span key={i} className="px-3 py-1 bg-gray-700 rounded-lg font-mono text-sm">
{file}
</span>
))}
</div>
</div>
)}
{/* Suggested Fix */}
{issue.suggested_fix && (
<div className="bg-purple-500/10 border border-purple-500/30 rounded-xl p-6 mb-6">
<h2 className="font-semibold text-purple-400 mb-3">🔧 Suggested Fix</h2>
<pre className="whitespace-pre-wrap font-mono text-sm bg-gray-900 p-4 rounded-lg">
{issue.suggested_fix}
</pre>
</div>
)}
{/* Metadata */}
<div className="bg-gray-800 rounded-xl border border-gray-700 p-6">
<h2 className="font-semibold mb-3">Details</h2>
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="text-gray-400">Created:</span>
<span className="ml-2">{new Date(issue.created_at).toLocaleString()}</span>
</div>
{issue.analyzed_at && (
<div>
<span className="text-gray-400">Analyzed:</span>
<span className="ml-2">{new Date(issue.analyzed_at).toLocaleString()}</span>
</div>
)}
</div>
</div>
</div>
)
}

90
src/pages/Issues.tsx Normal file
View File

@ -0,0 +1,90 @@
import { useState } from 'react'
import { useQuery } from '@tanstack/react-query'
import { Link } from 'react-router-dom'
import { issuesApi, Issue } from '../services/api'
export default function Issues() {
const [filter, setFilter] = useState('')
const { data: issues, isLoading } = useQuery({
queryKey: ['issues', filter],
queryFn: () => issuesApi.list(filter || undefined),
refetchInterval: 10000,
})
return (
<div className="p-6">
<div className="flex justify-between items-center mb-6">
<h1 className="text-2xl font-bold">Issues</h1>
<select
value={filter}
onChange={(e) => setFilter(e.target.value)}
className="bg-gray-700 border border-gray-600 rounded-lg px-3 py-2"
>
<option value="">All Status</option>
<option value="pending">Pending</option>
<option value="analyzed">Analyzed</option>
<option value="error">Error</option>
</select>
</div>
<div className="bg-gray-800 rounded-xl border border-gray-700">
{isLoading ? (
<div className="p-8 text-center text-gray-500">Loading...</div>
) : !issues?.length ? (
<div className="p-8 text-center text-gray-500">No issues found</div>
) : (
<div className="divide-y divide-gray-700">
{issues.map(issue => (
<IssueRow key={issue.id} issue={issue} />
))}
</div>
)}
</div>
</div>
)
}
function IssueRow({ issue }: { issue: Issue }) {
const statusStyles = {
analyzed: 'bg-green-500/20 text-green-400',
pending: 'bg-yellow-500/20 text-yellow-400',
error: 'bg-red-500/20 text-red-400',
}
return (
<Link
to={`/issues/${issue.id}`}
className="p-4 block hover:bg-gray-700/50 transition-colors"
>
<div className="flex justify-between items-start">
<div className="flex-1">
<div className="flex items-center gap-2">
<span className="font-mono text-blue-400 text-sm">
{issue.external_key || `#${issue.id}`}
</span>
<span className={`px-2 py-0.5 rounded text-xs ${statusStyles[issue.status] || 'bg-gray-500/20 text-gray-400'}`}>
{issue.status}
</span>
<span className="text-xs text-gray-500">{issue.source}</span>
</div>
<h3 className="font-medium mt-1">{issue.title}</h3>
<p className="text-sm text-gray-400 mt-1 line-clamp-2">{issue.description}</p>
</div>
{issue.confidence && (
<div className="ml-4 text-right">
<div className="w-24 bg-gray-700 rounded-full h-2">
<div
className="bg-green-500 h-2 rounded-full"
style={{ width: `${issue.confidence * 100}%` }}
/>
</div>
<span className="text-xs text-gray-400 mt-1">
{Math.round(issue.confidence * 100)}%
</span>
</div>
)}
</div>
</Link>
)
}

118
src/pages/Repositories.tsx Normal file
View File

@ -0,0 +1,118 @@
export default function Repositories() {
const repos = [
{
name: 'cobol-sample-app',
url: 'https://gitea.startdata.com.br/startdata/cobol-sample-app',
files: 4,
language: 'COBOL',
status: 'indexed',
},
]
return (
<div className="p-6">
<div className="flex justify-between items-center mb-6">
<h1 className="text-2xl font-bold">Repositories</h1>
<button className="bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded-lg flex items-center gap-2">
<span>+</span>
<span>Add Repository</span>
</button>
</div>
<div className="bg-gray-800 rounded-xl border border-gray-700">
{repos.map((repo, i) => (
<div key={i} className="p-4 flex items-center justify-between border-b border-gray-700 last:border-0">
<div className="flex items-center gap-4">
<div className="w-12 h-12 bg-gray-700 rounded-lg flex items-center justify-center">
<span className="text-2xl">📁</span>
</div>
<div>
<h3 className="font-semibold">{repo.name}</h3>
<p className="text-sm text-gray-400">{repo.url}</p>
</div>
</div>
<div className="flex items-center gap-6">
<div className="text-center">
<p className="text-lg font-semibold">{repo.files}</p>
<p className="text-xs text-gray-400">Files</p>
</div>
<div className="text-center">
<p className="text-lg font-semibold">{repo.language}</p>
<p className="text-xs text-gray-400">Language</p>
</div>
<span className="px-3 py-1 bg-green-500/20 text-green-400 rounded-full text-sm">
{repo.status}
</span>
</div>
</div>
))}
</div>
{/* Integrations */}
<h2 className="text-xl font-bold mt-8 mb-4">Integrations</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<IntegrationCard
name="Gitea"
icon="📦"
status="connected"
description="Self-hosted Git service"
/>
<IntegrationCard
name="GitHub"
icon="🐙"
status="available"
description="GitHub repositories"
/>
<IntegrationCard
name="GitLab"
icon="🦊"
status="available"
description="GitLab repositories"
/>
<IntegrationCard
name="Bitbucket"
icon="🪣"
status="available"
description="Atlassian Bitbucket"
/>
<IntegrationCard
name="Azure DevOps"
icon="🔷"
status="available"
description="Azure Repos"
/>
</div>
</div>
)
}
function IntegrationCard({ name, icon, status, description }: {
name: string
icon: string
status: 'connected' | 'available'
description: string
}) {
return (
<div className="bg-gray-800 rounded-xl border border-gray-700 p-4">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-3">
<span className="text-2xl">{icon}</span>
<h3 className="font-semibold">{name}</h3>
</div>
<span className={`px-2 py-1 rounded text-xs ${
status === 'connected'
? 'bg-green-500/20 text-green-400'
: 'bg-gray-500/20 text-gray-400'
}`}>
{status}
</span>
</div>
<p className="text-sm text-gray-400">{description}</p>
{status === 'available' && (
<button className="mt-3 text-sm text-blue-400 hover:text-blue-300">
Connect
</button>
)}
</div>
)
}

119
src/pages/Settings.tsx Normal file
View File

@ -0,0 +1,119 @@
export default function Settings() {
return (
<div className="p-6">
<h1 className="text-2xl font-bold mb-6">Settings</h1>
{/* Webhook Endpoints */}
<div className="bg-gray-800 rounded-xl border border-gray-700 p-6 mb-6">
<h2 className="font-semibold mb-4">Webhook Endpoints</h2>
<div className="space-y-3">
<EndpointRow
method="POST"
path="/api/webhook/tickethub"
description="Receive ticket events from TicketHub"
/>
<EndpointRow
method="POST"
path="/api/webhook/jira"
description="Receive issue events from JIRA"
/>
<EndpointRow
method="POST"
path="/api/webhook/servicenow"
description="Receive incident events from ServiceNow"
/>
</div>
</div>
{/* Issue Trackers */}
<div className="bg-gray-800 rounded-xl border border-gray-700 p-6 mb-6">
<h2 className="font-semibold mb-4">Issue Tracker Integrations</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<TrackerCard name="TicketHub" icon="🎫" status="active" />
<TrackerCard name="JIRA" icon="🔵" status="ready" />
<TrackerCard name="ServiceNow" icon="🟢" status="ready" />
<TrackerCard name="Azure DevOps" icon="🔷" status="ready" />
</div>
</div>
{/* AI Settings */}
<div className="bg-gray-800 rounded-xl border border-gray-700 p-6">
<h2 className="font-semibold mb-4">AI Configuration</h2>
<div className="space-y-4">
<div>
<label className="block text-sm text-gray-400 mb-1">LLM Provider</label>
<select className="w-full bg-gray-700 border border-gray-600 rounded-lg px-3 py-2">
<option>OpenRouter (Free Tier)</option>
<option>OpenAI</option>
<option>Anthropic</option>
</select>
</div>
<div>
<label className="block text-sm text-gray-400 mb-1">Model</label>
<select className="w-full bg-gray-700 border border-gray-600 rounded-lg px-3 py-2">
<option>meta-llama/llama-3.3-70b-instruct (Free)</option>
<option>gpt-4-turbo</option>
<option>claude-3-opus</option>
</select>
</div>
<div>
<label className="block text-sm text-gray-400 mb-1">Confidence Threshold</label>
<input
type="range"
min="0"
max="100"
defaultValue="70"
className="w-full"
/>
<div className="flex justify-between text-xs text-gray-500">
<span>0%</span>
<span>70%</span>
<span>100%</span>
</div>
</div>
</div>
</div>
</div>
)
}
function EndpointRow({ method, path, description }: {
method: string
path: string
description: string
}) {
return (
<div className="flex items-center gap-4 p-3 bg-gray-700/50 rounded-lg">
<span className="px-2 py-1 bg-blue-500/20 text-blue-400 rounded text-xs font-mono">
{method}
</span>
<code className="flex-1 font-mono text-sm">{path}</code>
<span className="text-sm text-gray-400">{description}</span>
<button className="text-gray-400 hover:text-white">
📋
</button>
</div>
)
}
function TrackerCard({ name, icon, status }: {
name: string
icon: string
status: 'active' | 'ready'
}) {
return (
<div className="flex items-center justify-between p-4 bg-gray-700/50 rounded-lg">
<div className="flex items-center gap-3">
<span className="text-2xl">{icon}</span>
<span className="font-medium">{name}</span>
</div>
<span className={`px-2 py-1 rounded text-xs ${
status === 'active'
? 'bg-green-500/20 text-green-400'
: 'bg-gray-500/20 text-gray-400'
}`}>
{status}
</span>
</div>
)
}

59
src/services/api.ts Normal file
View File

@ -0,0 +1,59 @@
import axios from 'axios'
const API_URL = import.meta.env.VITE_API_URL || '/api'
export const api = axios.create({
baseURL: API_URL,
})
export interface Issue {
id: number
external_key: string
source: string
title: string
description: string
status: 'pending' | 'analyzed' | 'error'
analysis?: string
affected_files?: string
suggested_fix?: string
confidence?: number
created_at: string
analyzed_at?: string
}
export interface Stats {
total: number
analyzed: number
pending: number
error: number
prs_created: number
avg_confidence: number
}
export const issuesApi = {
list: async (status?: string): Promise<Issue[]> => {
const params = status ? { status } : {}
const { data } = await api.get('/issues', { params })
return data
},
get: async (id: number): Promise<Issue> => {
const { data } = await api.get(`/issues/${id}`)
return data
},
getStats: async (): Promise<Stats> => {
const issues = await issuesApi.list()
const analyzed = issues.filter(i => i.status === 'analyzed')
return {
total: issues.length,
analyzed: analyzed.length,
pending: issues.filter(i => i.status === 'pending').length,
error: issues.filter(i => i.status === 'error').length,
prs_created: analyzed.filter(i => i.suggested_fix).length,
avg_confidence: analyzed.length
? Math.round(analyzed.reduce((a, i) => a + (i.confidence || 0), 0) / analyzed.length * 100)
: 0
}
}
}

8
tailwind.config.js Normal file
View File

@ -0,0 +1,8 @@
/** @type {import('tailwindcss').Config} */
export default {
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
theme: {
extend: {},
},
plugins: [],
}

21
tsconfig.json Normal file
View File

@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

10
tsconfig.node.json Normal file
View File

@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

11
vite.config.ts Normal file
View File

@ -0,0 +1,11 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
server: {
proxy: {
'/api': 'http://localhost:8000'
}
}
})