feat: Professional dashboard UI
- Stats cards (Total, Analyzed, PRs, Confidence) - Issue list with filters - Issue detail modal - Integrations status panel - Repository info panel - Webhook endpoints reference
This commit is contained in:
parent
78ebb9b9d8
commit
df998cc581
362
api/main_v2.py
362
api/main_v2.py
|
|
@ -343,46 +343,329 @@ async def get_issue(issue_id: int):
|
||||||
return dict(row)
|
return dict(row)
|
||||||
|
|
||||||
# Dashboard HTML
|
# Dashboard HTML
|
||||||
|
|
||||||
|
# Dashboard HTML
|
||||||
|
from app.dashboard_html import DASHBOARD_HTML if False else None # noqa
|
||||||
|
|
||||||
DASHBOARD_HTML = """<!DOCTYPE html>
|
DASHBOARD_HTML = """<!DOCTYPE html>
|
||||||
<html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
<html lang="en">
|
||||||
<title>JIRA AI Fixer</title><script src="https://cdn.tailwindcss.com"></script></head>
|
<head>
|
||||||
<body class="bg-gray-900 text-white">
|
<meta charset="UTF-8">
|
||||||
<div class="min-h-screen">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<header class="bg-gray-800 border-b border-gray-700 p-4">
|
<title>JIRA AI Fixer</title>
|
||||||
<div class="max-w-6xl mx-auto flex items-center justify-between">
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
<div class="flex items-center gap-3"><span class="text-3xl">🤖</span><h1 class="text-xl font-bold">JIRA AI Fixer</h1></div>
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||||
<span class="text-sm text-gray-400">Intelligent Support Case Resolution</span>
|
<style>
|
||||||
</div></header>
|
body { font-family: 'Inter', system-ui, sans-serif; }
|
||||||
<main class="max-w-6xl mx-auto p-6">
|
.gradient-bg { background: linear-gradient(135deg, #1e3a8a 0%, #7c3aed 100%); }
|
||||||
<div class="grid grid-cols-3 gap-4 mb-6">
|
</style>
|
||||||
<div class="bg-gray-800 rounded-lg p-4"><div class="text-3xl font-bold" id="total">-</div><div class="text-gray-400">Total Issues</div></div>
|
</head>
|
||||||
<div class="bg-gray-800 rounded-lg p-4"><div class="text-3xl font-bold text-green-400" id="analyzed">-</div><div class="text-gray-400">Analyzed</div></div>
|
<body class="bg-gray-900 text-white min-h-screen">
|
||||||
<div class="bg-gray-800 rounded-lg p-4"><div class="text-3xl font-bold text-yellow-400" id="pending">-</div><div class="text-gray-400">Pending</div></div>
|
<!-- Header -->
|
||||||
</div>
|
<header class="gradient-bg border-b border-white/10">
|
||||||
<div class="bg-gray-800 rounded-lg">
|
<div class="max-w-7xl mx-auto px-6 py-4">
|
||||||
<div class="p-4 border-b border-gray-700"><h2 class="font-semibold">Recent Issues</h2></div>
|
<div class="flex items-center justify-between">
|
||||||
<div id="issues" class="divide-y divide-gray-700"></div>
|
<div class="flex items-center gap-3">
|
||||||
</div>
|
<span class="text-3xl">🤖</span>
|
||||||
</main></div>
|
<div>
|
||||||
<script>
|
<h1 class="text-xl font-bold">JIRA AI Fixer</h1>
|
||||||
async function load(){
|
<p class="text-sm text-blue-200">Intelligent Support Case Resolution</p>
|
||||||
const r=await fetch('/api/issues');const issues=await r.json();
|
</div>
|
||||||
document.getElementById('total').textContent=issues.length;
|
</div>
|
||||||
document.getElementById('analyzed').textContent=issues.filter(i=>i.status==='analyzed').length;
|
<div class="flex items-center gap-4">
|
||||||
document.getElementById('pending').textContent=issues.filter(i=>i.status==='pending').length;
|
<span class="px-3 py-1 bg-green-500/20 text-green-400 rounded-full text-sm" id="status">● Online</span>
|
||||||
document.getElementById('issues').innerHTML=issues.length?issues.map(i=>`
|
</div>
|
||||||
<div class="p-4 hover:bg-gray-700/50">
|
</div>
|
||||||
<div class="flex justify-between items-start">
|
</div>
|
||||||
<div><span class="text-blue-400 font-mono">${i.external_key||'#'+i.id}</span>
|
</header>
|
||||||
<span class="ml-2">${i.title}</span></div>
|
|
||||||
<span class="px-2 py-1 rounded text-xs ${i.status==='analyzed'?'bg-green-500/20 text-green-400':i.status==='error'?'bg-red-500/20 text-red-400':'bg-yellow-500/20 text-yellow-400'}">${i.status}</span>
|
<main class="max-w-7xl mx-auto px-6 py-8">
|
||||||
</div>
|
<!-- Stats Grid -->
|
||||||
${i.confidence?`<div class="mt-2 text-sm text-gray-400">Confidence: ${Math.round(i.confidence*100)}%</div>`:''}
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
||||||
${i.analysis?`<div class="mt-2 text-sm text-gray-300 line-clamp-2">${i.analysis.substring(0,200)}...</div>`:''}
|
<div class="bg-gray-800 rounded-xl p-6 border border-gray-700">
|
||||||
</div>`).join(''):'<div class="p-8 text-center text-gray-500">No issues yet</div>';
|
<div class="flex items-center justify-between">
|
||||||
}
|
<div>
|
||||||
load();setInterval(load,5000);
|
<p class="text-gray-400 text-sm">Total Issues</p>
|
||||||
</script></body></html>"""
|
<p class="text-3xl font-bold mt-1" id="stat-total">0</p>
|
||||||
|
</div>
|
||||||
|
<div class="w-12 h-12 bg-blue-500/20 rounded-lg flex items-center justify-center">
|
||||||
|
<span class="text-2xl">📋</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-gray-800 rounded-xl p-6 border border-gray-700">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p class="text-gray-400 text-sm">Analyzed</p>
|
||||||
|
<p class="text-3xl font-bold mt-1 text-green-400" id="stat-analyzed">0</p>
|
||||||
|
</div>
|
||||||
|
<div class="w-12 h-12 bg-green-500/20 rounded-lg flex items-center justify-center">
|
||||||
|
<span class="text-2xl">✅</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-gray-800 rounded-xl p-6 border border-gray-700">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p class="text-gray-400 text-sm">PRs Created</p>
|
||||||
|
<p class="text-3xl font-bold mt-1 text-purple-400" id="stat-prs">0</p>
|
||||||
|
</div>
|
||||||
|
<div class="w-12 h-12 bg-purple-500/20 rounded-lg flex items-center justify-center">
|
||||||
|
<span class="text-2xl">🔀</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-gray-800 rounded-xl p-6 border border-gray-700">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p class="text-gray-400 text-sm">Avg Confidence</p>
|
||||||
|
<p class="text-3xl font-bold mt-1 text-yellow-400" id="stat-confidence">0%</p>
|
||||||
|
</div>
|
||||||
|
<div class="w-12 h-12 bg-yellow-500/20 rounded-lg flex items-center justify-center">
|
||||||
|
<span class="text-2xl">🎯</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main Content Grid -->
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
|
<!-- Issues List -->
|
||||||
|
<div class="lg:col-span-2 bg-gray-800 rounded-xl border border-gray-700">
|
||||||
|
<div class="p-4 border-b border-gray-700 flex items-center justify-between">
|
||||||
|
<h2 class="font-semibold">Recent Issues</h2>
|
||||||
|
<select id="filter-status" onchange="loadIssues()" class="bg-gray-700 border border-gray-600 rounded-lg px-3 py-1 text-sm">
|
||||||
|
<option value="">All Status</option>
|
||||||
|
<option value="pending">Pending</option>
|
||||||
|
<option value="analyzed">Analyzed</option>
|
||||||
|
<option value="error">Error</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div id="issues-list" class="divide-y divide-gray-700 max-h-[600px] overflow-y-auto">
|
||||||
|
<div class="p-8 text-center text-gray-500">Loading...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<div class="space-y-6">
|
||||||
|
<!-- Integrations -->
|
||||||
|
<div class="bg-gray-800 rounded-xl border border-gray-700 p-4">
|
||||||
|
<h3 class="font-semibold mb-4">Integrations</h3>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span>🎫</span>
|
||||||
|
<span class="text-sm">TicketHub</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-xs px-2 py-1 bg-green-500/20 text-green-400 rounded">Active</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span>📦</span>
|
||||||
|
<span class="text-sm">Gitea</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-xs px-2 py-1 bg-green-500/20 text-green-400 rounded">Active</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span>🔵</span>
|
||||||
|
<span class="text-sm">JIRA</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-xs px-2 py-1 bg-gray-500/20 text-gray-400 rounded">Ready</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span>⚙️</span>
|
||||||
|
<span class="text-sm">ServiceNow</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-xs px-2 py-1 bg-gray-500/20 text-gray-400 rounded">Ready</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Connected Repos -->
|
||||||
|
<div class="bg-gray-800 rounded-xl border border-gray-700 p-4">
|
||||||
|
<h3 class="font-semibold mb-4">Repositories</h3>
|
||||||
|
<div class="space-y-3" id="repos-list">
|
||||||
|
<div class="flex items-center gap-2 p-2 bg-gray-700/50 rounded-lg">
|
||||||
|
<span>📁</span>
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-medium">cobol-sample-app</p>
|
||||||
|
<p class="text-xs text-gray-400">4 COBOL files indexed</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quick Actions -->
|
||||||
|
<div class="bg-gray-800 rounded-xl border border-gray-700 p-4">
|
||||||
|
<h3 class="font-semibold mb-4">Webhook Endpoints</h3>
|
||||||
|
<div class="space-y-2 text-xs">
|
||||||
|
<div class="p-2 bg-gray-700/50 rounded font-mono break-all">
|
||||||
|
POST /api/webhook/tickethub
|
||||||
|
</div>
|
||||||
|
<div class="p-2 bg-gray-700/50 rounded font-mono break-all">
|
||||||
|
POST /api/webhook/jira
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Issue Detail Modal -->
|
||||||
|
<div id="issue-modal" class="fixed inset-0 bg-black/70 hidden items-center justify-center z-50 p-4">
|
||||||
|
<div class="bg-gray-800 rounded-xl w-full max-w-2xl max-h-[90vh] overflow-hidden border border-gray-700">
|
||||||
|
<div class="p-4 border-b border-gray-700 flex justify-between items-center">
|
||||||
|
<div>
|
||||||
|
<span class="font-mono text-blue-400" id="modal-key"></span>
|
||||||
|
<span class="ml-2 px-2 py-1 rounded text-xs" id="modal-status"></span>
|
||||||
|
</div>
|
||||||
|
<button onclick="hideModal()" class="text-gray-400 hover:text-white">
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="p-6 overflow-y-auto max-h-[70vh]" id="modal-content"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
loadIssues();
|
||||||
|
setInterval(loadIssues, 10000);
|
||||||
|
|
||||||
|
async function loadIssues() {
|
||||||
|
const filter = document.getElementById('filter-status').value;
|
||||||
|
const url = filter ? '/api/issues?status=' + filter : '/api/issues';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const r = await fetch(url);
|
||||||
|
const issues = await r.json();
|
||||||
|
|
||||||
|
// Update stats
|
||||||
|
document.getElementById('stat-total').textContent = issues.length;
|
||||||
|
document.getElementById('stat-analyzed').textContent = issues.filter(i => i.status === 'analyzed').length;
|
||||||
|
|
||||||
|
// Count PRs (issues with suggested_fix that aren't errors)
|
||||||
|
const prs = issues.filter(i => i.status === 'analyzed' && i.suggested_fix).length;
|
||||||
|
document.getElementById('stat-prs').textContent = prs;
|
||||||
|
|
||||||
|
// Avg confidence
|
||||||
|
const analyzed = issues.filter(i => i.confidence);
|
||||||
|
const avgConf = analyzed.length ? Math.round(analyzed.reduce((a, i) => a + (i.confidence || 0), 0) / analyzed.length * 100) : 0;
|
||||||
|
document.getElementById('stat-confidence').textContent = avgConf + '%';
|
||||||
|
|
||||||
|
// Render list
|
||||||
|
const list = document.getElementById('issues-list');
|
||||||
|
if (!issues.length) {
|
||||||
|
list.innerHTML = '<div class="p-8 text-center text-gray-500">No issues found</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
list.innerHTML = issues.map(i => `
|
||||||
|
<div onclick="showIssue(${i.id})" class="p-4 hover:bg-gray-700/50 cursor-pointer">
|
||||||
|
<div class="flex justify-between items-start">
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="font-mono text-blue-400 text-sm">${i.external_key || '#' + i.id}</span>
|
||||||
|
<span class="text-xs px-2 py-0.5 rounded ${getStatusClass(i.status)}">${i.status}</span>
|
||||||
|
</div>
|
||||||
|
<h4 class="font-medium mt-1">${i.title}</h4>
|
||||||
|
${i.confidence ? `<div class="mt-2 flex items-center gap-2">
|
||||||
|
<div class="flex-1 bg-gray-700 rounded-full h-2">
|
||||||
|
<div class="bg-green-500 h-2 rounded-full" style="width: ${Math.round(i.confidence * 100)}%"></div>
|
||||||
|
</div>
|
||||||
|
<span class="text-xs text-gray-400">${Math.round(i.confidence * 100)}%</span>
|
||||||
|
</div>` : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStatusClass(status) {
|
||||||
|
switch(status) {
|
||||||
|
case 'analyzed': return 'bg-green-500/20 text-green-400';
|
||||||
|
case 'pending': return 'bg-yellow-500/20 text-yellow-400';
|
||||||
|
case 'error': return 'bg-red-500/20 text-red-400';
|
||||||
|
default: return 'bg-gray-500/20 text-gray-400';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function showIssue(id) {
|
||||||
|
const r = await fetch('/api/issues/' + id);
|
||||||
|
const issue = await r.json();
|
||||||
|
|
||||||
|
document.getElementById('modal-key').textContent = issue.external_key || '#' + issue.id;
|
||||||
|
document.getElementById('modal-status').textContent = issue.status;
|
||||||
|
document.getElementById('modal-status').className = 'ml-2 px-2 py-1 rounded text-xs ' + getStatusClass(issue.status);
|
||||||
|
|
||||||
|
let affectedFiles = [];
|
||||||
|
try {
|
||||||
|
affectedFiles = JSON.parse(issue.affected_files || '[]');
|
||||||
|
} catch(e) {}
|
||||||
|
|
||||||
|
document.getElementById('modal-content').innerHTML = `
|
||||||
|
<h3 class="text-lg font-semibold">${issue.title}</h3>
|
||||||
|
<p class="text-gray-400 text-sm mt-1">Source: ${issue.source}</p>
|
||||||
|
|
||||||
|
<div class="mt-4 p-4 bg-gray-700/50 rounded-lg">
|
||||||
|
<h4 class="text-sm font-medium text-gray-300 mb-2">Description</h4>
|
||||||
|
<pre class="whitespace-pre-wrap text-sm">${issue.description || 'N/A'}</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${issue.analysis ? `
|
||||||
|
<div class="mt-4 p-4 bg-green-500/10 border border-green-500/30 rounded-lg">
|
||||||
|
<h4 class="text-sm font-medium text-green-400 mb-2">🔍 Analysis</h4>
|
||||||
|
<pre class="whitespace-pre-wrap text-sm">${issue.analysis}</pre>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
${affectedFiles.length ? `
|
||||||
|
<div class="mt-4">
|
||||||
|
<h4 class="text-sm font-medium text-gray-300 mb-2">📁 Affected Files</h4>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
${affectedFiles.map(f => `<span class="px-2 py-1 bg-gray-700 rounded text-sm font-mono">${f}</span>`).join('')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
${issue.suggested_fix ? `
|
||||||
|
<div class="mt-4 p-4 bg-purple-500/10 border border-purple-500/30 rounded-lg">
|
||||||
|
<h4 class="text-sm font-medium text-purple-400 mb-2">🔧 Suggested Fix</h4>
|
||||||
|
<pre class="whitespace-pre-wrap text-sm font-mono bg-gray-900 p-3 rounded">${issue.suggested_fix}</pre>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
${issue.confidence ? `
|
||||||
|
<div class="mt-4 flex items-center gap-3">
|
||||||
|
<span class="text-sm text-gray-400">Confidence:</span>
|
||||||
|
<div class="flex-1 bg-gray-700 rounded-full h-3">
|
||||||
|
<div class="bg-green-500 h-3 rounded-full" style="width: ${Math.round(issue.confidence * 100)}%"></div>
|
||||||
|
</div>
|
||||||
|
<span class="font-bold text-green-400">${Math.round(issue.confidence * 100)}%</span>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
<div class="mt-4 text-xs text-gray-500">
|
||||||
|
Created: ${new Date(issue.created_at).toLocaleString()}
|
||||||
|
${issue.analyzed_at ? `<br>Analyzed: ${new Date(issue.analyzed_at).toLocaleString()}` : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.getElementById('issue-modal').classList.remove('hidden');
|
||||||
|
document.getElementById('issue-modal').classList.add('flex');
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideModal() {
|
||||||
|
document.getElementById('issue-modal').classList.add('hidden');
|
||||||
|
document.getElementById('issue-modal').classList.remove('flex');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>"""
|
||||||
|
|
||||||
@app.get("/", response_class=HTMLResponse)
|
@app.get("/", response_class=HTMLResponse)
|
||||||
async def dashboard():
|
async def dashboard():
|
||||||
|
|
@ -391,7 +674,6 @@ async def dashboard():
|
||||||
@app.get("/dashboard", response_class=HTMLResponse)
|
@app.get("/dashboard", response_class=HTMLResponse)
|
||||||
async def dashboard_alt():
|
async def dashboard_alt():
|
||||||
return DASHBOARD_HTML
|
return DASHBOARD_HTML
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# GIT INTEGRATION - Create Branch and PR
|
# GIT INTEGRATION - Create Branch and PR
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue