bmad-method-mcp
Version:
Breakthrough Method of Agile AI-driven Development with Enhanced MCP Integration
496 lines (468 loc) • 25.4 kB
HTML
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BMAD Project Dashboard</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f7fa; color: #333; }
.header { background: #2563eb; color: white; padding: 1rem 2rem; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.header h1 { font-size: 1.5rem; font-weight: 600; }
.container { max-width: 1200px; margin: 0 auto; padding: 2rem; }
.sprint-banner { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 1.5rem; border-radius: 8px; margin-bottom: 2rem; display: flex; justify-content: space-between; align-items: center; }
.sprint-info h2 { margin-bottom: 0.5rem; font-size: 1.25rem; }
.sprint-goal { opacity: 0.9; font-size: 0.95rem; }
.dashboard-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 2rem; margin-bottom: 2rem; }
.card { background: white; border-radius: 8px; padding: 1.5rem; box-shadow: 0 2px 4px rgba(0,0,0,0.1); border: 1px solid #e5e7eb; }
.card h2 { color: #1f2937; margin-bottom: 1rem; font-size: 1.25rem; font-weight: 600; }
.progress-bar { background: #e5e7eb; height: 12px; border-radius: 6px; overflow: hidden; margin: 0.5rem 0; }
.progress-fill { background: #10b981; height: 100%; transition: width 0.3s ease; }
.stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; margin-bottom: 1rem; }
.stat { text-align: center; padding: 1rem; background: #f9fafb; border-radius: 6px; }
.stat-number { font-size: 1.5rem; font-weight: 700; color: #1f2937; }
.stat-label { font-size: 0.875rem; color: #6b7280; margin-top: 0.25rem; }
.tabs { display: flex; justify-content: space-between; align-items: center; border-bottom: 2px solid #e5e7eb; margin-bottom: 1.5rem; }
.tab-group { display: flex; }
.tab { padding: 0.75rem 1.5rem; cursor: pointer; border-bottom: 2px solid transparent; font-weight: 500; color: #6b7280; transition: all 0.2s; }
.tab.active { color: #2563eb; border-bottom-color: #2563eb; }
.tab-actions { display: flex; gap: 1rem; }
.tab-content { display: none; }
.tab-content.active { display: block; }
.table { width: 100%; border-collapse: collapse; }
.table th, .table td { padding: 0.75rem; text-align: left; border-bottom: 1px solid #e5e7eb; }
.table th { background: #f9fafb; font-weight: 600; color: #374151; }
.status-badge { padding: 0.25rem 0.75rem; border-radius: 12px; font-size: 0.875rem; font-weight: 500; }
.status-todo { background: #fef3c7; color: #92400e; }
.status-in-progress { background: #dbeafe; color: #1d4ed8; }
.status-done { background: #d1fae5; color: #065f46; }
.status-draft { background: #fef3c7; color: #92400e; }
.status-approved { background: #d1fae5; color: #065f46; }
.status-in-review { background: #dbeafe; color: #1d4ed8; }
.btn { padding: 0.5rem 1rem; border: none; border-radius: 6px; cursor: pointer; font-size: 0.875rem; font-weight: 500; transition: all 0.2s; text-decoration: none; display: inline-block; }
.btn-primary { background: #2563eb; color: white; }
.btn-primary:hover { background: #1d4ed8; }
.btn-success { background: #10b981; color: white; }
.btn-success:hover { background: #059669; }
.btn-danger { background: #ef4444; color: white; }
.btn-danger:hover { background: #dc2626; }
.btn-secondary { background: #6b7280; color: white; }
.btn-secondary:hover { background: #4b5563; }
.loading { text-align: center; color: #6b7280; padding: 2rem; }
.epic-item { background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 6px; padding: 1rem; margin-bottom: 1rem; }
.epic-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem; }
.epic-title { font-weight: 600; color: #1f2937; }
.epic-progress { font-size: 0.875rem; color: #6b7280; }
.modal { display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); }
.modal.show { display: flex; align-items: center; justify-content: center; }
.modal-content { background: white; padding: 2rem; border-radius: 8px; width: 90%; max-width: 500px; max-height: 80vh; overflow-y: auto; }
.modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem; }
.modal-title { font-size: 1.25rem; font-weight: 600; color: #1f2937; }
.close { font-size: 1.5rem; cursor: pointer; color: #6b7280; }
.close:hover { color: #1f2937; }
.form-group { margin-bottom: 1rem; }
.form-label { display: block; margin-bottom: 0.5rem; font-weight: 500; color: #374151; }
.form-input, .form-select, .form-textarea { width: 100%; padding: 0.5rem; border: 1px solid #d1d5db; border-radius: 6px; font-size: 0.875rem; }
.form-textarea { min-height: 100px; resize: vertical; }
.form-actions { display: flex; gap: 1rem; justify-content: flex-end; margin-top: 1.5rem; }
.action-buttons { display: flex; gap: 0.5rem; }
.no-sprint { text-align: center; color: #6b7280; font-style: italic; }
</style>
</head>
<body>
<div class="header">
<h1>🚀 BMAD Project Dashboard</h1>
</div>
<div class="container">
<div id="sprint-banner" class="sprint-banner">
<div class="sprint-info">
<div id="sprint-content">
<div class="loading no-sprint">No active sprint</div>
</div>
</div>
<button class="btn btn-secondary" onclick="openSprintModal()">Create Sprint</button>
</div>
<div class="dashboard-grid">
<div class="card">
<h2>📊 Project Progress</h2>
<div id="overall-progress"><div class="loading">Loading progress...</div></div>
</div>
<div class="card">
<h2>📈 Epic Overview</h2>
<div id="epic-overview"><div class="loading">Loading epics...</div></div>
</div>
</div>
<div class="card">
<div class="tabs">
<div class="tab-group">
<div class="tab active" onclick="showTab('tasks')">📋 Tasks</div>
<div class="tab" onclick="showTab('documents')">📄 Documents</div>
<div class="tab" onclick="showTab('epics')">📚 Epics</div>
</div>
<div class="tab-actions">
<button class="btn btn-primary" onclick="openTaskModal()">Add Task</button>
<button class="btn btn-primary" onclick="openDocumentModal()">Add Document</button>
<button class="btn btn-primary" onclick="openEpicModal()">Add Epic</button>
</div>
</div>
<div id="tasks-content" class="tab-content active">
<div id="tasks-table"><div class="loading">Loading tasks...</div></div>
</div>
<div id="documents-content" class="tab-content">
<div id="documents-table"><div class="loading">Loading documents...</div></div>
</div>
<div id="epics-content" class="tab-content">
<div id="epics-table"><div class="loading">Loading epics...</div></div>
</div>
</div>
</div>
<!-- Modals -->
<div id="sprintModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title">Create New Sprint</h3>
<span class="close" onclick="closeModal('sprintModal')">×</span>
</div>
<form id="sprintForm">
<div class="form-group">
<label class="form-label">Sprint Name</label>
<input type="text" class="form-input" name="name" required placeholder="e.g., Sprint 2024-Q1">
</div>
<div class="form-group">
<label class="form-label">Sprint Goal</label>
<textarea class="form-textarea" name="goal" required placeholder="What are the main objectives for this sprint?"></textarea>
</div>
<div class="form-group">
<label class="form-label">Start Date</label>
<input type="date" class="form-input" name="start_date">
</div>
<div class="form-group">
<label class="form-label">End Date</label>
<input type="date" class="form-input" name="end_date">
</div>
<div class="form-actions">
<button type="button" class="btn btn-secondary" onclick="closeModal('sprintModal')">Cancel</button>
<button type="submit" class="btn btn-primary">Create Sprint</button>
</div>
</form>
</div>
</div>
<div id="taskModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title" id="taskModalTitle">Add New Task</h3>
<span class="close" onclick="closeModal('taskModal')">×</span>
</div>
<form id="taskForm">
<input type="hidden" name="task_id">
<div class="form-group">
<label class="form-label">Epic Number</label>
<select class="form-select" name="epic_num" required id="taskEpicSelect">
<option value="">Select Epic</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Title</label>
<input type="text" class="form-input" name="title" required placeholder="Task title">
</div>
<div class="form-group">
<label class="form-label">Description</label>
<textarea class="form-textarea" name="description" required placeholder="Detailed task description and acceptance criteria"></textarea>
</div>
<div class="form-group">
<label class="form-label">Assignee</label>
<select class="form-select" name="assignee">
<option value="">Unassigned</option>
<option value="dev">Developer</option>
<option value="qa">QA</option>
<option value="sm">Scrum Master</option>
<option value="pm">Product Manager</option>
<option value="architect">Architect</option>
<option value="designer">Designer</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Priority</label>
<select class="form-select" name="priority">
<option value="LOW">Low</option>
<option value="MEDIUM" selected>Medium</option>
<option value="HIGH">High</option>
<option value="CRITICAL">Critical</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Status</label>
<select class="form-select" name="status">
<option value="TODO" selected>TODO</option>
<option value="IN_PROGRESS">In Progress</option>
<option value="DONE">Done</option>
</select>
</div>
<div class="form-actions">
<button type="button" class="btn btn-secondary" onclick="closeModal('taskModal')">Cancel</button>
<button type="submit" class="btn btn-primary" id="taskSubmitBtn">Create Task</button>
</div>
</form>
</div>
</div>
<script>
let currentData = { tasks: [], documents: [], epics: [], progress: null, currentSprint: null };
function openModal(modalId) { document.getElementById(modalId).classList.add('show'); }
function closeModal(modalId) {
document.getElementById(modalId).classList.remove('show');
const form = document.querySelector(`#${modalId} form`);
if (form) form.reset();
}
function openSprintModal() { openModal('sprintModal'); }
function openTaskModal(taskData = null) {
if (taskData) {
document.getElementById('taskModalTitle').textContent = 'Edit Task';
document.getElementById('taskSubmitBtn').textContent = 'Update Task';
const form = document.getElementById('taskForm');
form.task_id.value = taskData.id;
form.epic_num.value = taskData.epic_num;
form.title.value = taskData.title;
form.description.value = taskData.description;
form.assignee.value = taskData.assignee || '';
form.priority.value = taskData.priority;
form.status.value = taskData.status;
} else {
document.getElementById('taskModalTitle').textContent = 'Add New Task';
document.getElementById('taskSubmitBtn').textContent = 'Create Task';
}
populateEpicSelect();
openModal('taskModal');
}
function populateEpicSelect() {
const select = document.getElementById('taskEpicSelect');
select.innerHTML = '<option value="">Select Epic</option>';
currentData.epics.forEach(epic => {
const option = document.createElement('option');
option.value = epic.epic_num;
option.textContent = `Epic ${epic.epic_num}: ${epic.title}`;
select.appendChild(option);
});
}
function showTab(tabName) {
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
document.querySelectorAll('.tab').forEach(tab => tab.classList.remove('active'));
document.getElementById(tabName + '-content').classList.add('active');
event.target.classList.add('active');
}
async function fetchCurrentSprint() {
try {
const response = await fetch('/api/sprints/current');
const data = await response.json();
currentData.currentSprint = data;
renderCurrentSprint(data);
} catch (error) { console.error('Error fetching current sprint:', error); }
}
async function fetchProgress() {
try {
const response = await fetch('/api/progress');
const data = await response.json();
currentData.progress = data;
renderProgress(data);
} catch (error) { console.error('Error fetching progress:', error); }
}
async function fetchTasks() {
try {
const response = await fetch('/api/tasks');
const data = await response.json();
currentData.tasks = data;
renderTasks(data);
} catch (error) { console.error('Error fetching tasks:', error); }
}
async function fetchEpics() {
try {
const response = await fetch('/api/epics');
const data = await response.json();
currentData.epics = data;
renderEpics(data);
} catch (error) { console.error('Error fetching epics:', error); }
}
function renderCurrentSprint(sprint) {
const container = document.getElementById('sprint-content');
if (!sprint) {
container.innerHTML = '<div class="no-sprint">No active sprint</div>';
return;
}
container.innerHTML = `
<h2>${sprint.name}</h2>
<div class="sprint-goal">${sprint.goal}</div>
${sprint.start_date && sprint.end_date ? `
<div class="sprint-goal" style="margin-top: 0.5rem; font-size: 0.85rem;">
${new Date(sprint.start_date).toLocaleDateString()} - ${new Date(sprint.end_date).toLocaleDateString()}
</div>
` : ''}
`;
}
function renderProgress(data) {
const container = document.getElementById('overall-progress');
const { overall } = data;
container.innerHTML = `
<div class="stats">
<div class="stat">
<div class="stat-number">${overall.total_tasks || 0}</div>
<div class="stat-label">Total Tasks</div>
</div>
<div class="stat">
<div class="stat-number">${overall.completed_tasks || 0}</div>
<div class="stat-label">Completed</div>
</div>
<div class="stat">
<div class="stat-number">${overall.completion_percentage || 0}%</div>
<div class="stat-label">Progress</div>
</div>
</div>
<div class="progress-bar">
<div class="progress-fill" style="width: ${overall.completion_percentage || 0}%"></div>
</div>
`;
const epicContainer = document.getElementById('epic-overview');
if (data.epics && data.epics.length > 0) {
epicContainer.innerHTML = data.epics.map(epic => `
<div class="epic-item">
<div class="epic-header">
<span class="epic-title">Epic ${epic.epic_num}: ${epic.title}</span>
<span class="epic-progress">${epic.completion_percentage || 0}%</span>
</div>
<div class="progress-bar">
<div class="progress-fill" style="width: ${epic.completion_percentage || 0}%"></div>
</div>
</div>
`).join('');
} else {
epicContainer.innerHTML = '<div class="loading">No epics found</div>';
}
}
function renderTasks(tasks) {
const container = document.getElementById('tasks-table');
if (tasks.length === 0) {
container.innerHTML = '<div class="loading">No tasks found</div>';
return;
}
container.innerHTML = `
<table class="table">
<thead>
<tr><th>ID</th><th>Title</th><th>Epic</th><th>Status</th><th>Assignee</th><th>Actions</th></tr>
</thead>
<tbody>
${tasks.map(task => `
<tr>
<td>${task.id}</td>
<td>${task.title}</td>
<td>Epic ${task.epic_num}</td>
<td><span class="status-badge status-${task.status.toLowerCase().replace('_', '-')}">${task.status}</span></td>
<td>${task.assignee || 'Unassigned'}</td>
<td>
<div class="action-buttons">
${task.status === 'TODO' ? `<button class="btn btn-success" onclick="approveTask('${task.id}')">Approve</button>` : ''}
<button class="btn btn-primary" onclick="editTask('${task.id}')">Edit</button>
<button class="btn btn-danger" onclick="deleteTask('${task.id}')">Delete</button>
</div>
</td>
</tr>
`).join('')}
</tbody>
</table>
`;
}
function renderEpics(epics) {
const container = document.getElementById('epics-table');
if (epics.length === 0) {
container.innerHTML = '<div class="loading">No epics found</div>';
return;
}
container.innerHTML = `
<table class="table">
<thead>
<tr><th>Epic #</th><th>Title</th><th>Description</th><th>Priority</th><th>Created</th><th>Actions</th></tr>
</thead>
<tbody>
${epics.map(epic => `
<tr>
<td>Epic ${epic.epic_num}</td>
<td>${epic.title}</td>
<td>${epic.description}</td>
<td><span class="status-badge status-${epic.priority.toLowerCase()}">${epic.priority}</span></td>
<td>${new Date(epic.created_at).toLocaleDateString()}</td>
<td>
<div class="action-buttons">
<button class="btn btn-primary" onclick="editEpic(${epic.id})">Edit</button>
<button class="btn btn-danger" onclick="deleteEpic(${epic.id})">Delete</button>
</div>
</td>
</tr>
`).join('')}
</tbody>
</table>
`;
}
async function approveTask(taskId) {
try {
const response = await fetch(`/api/tasks/${taskId}/approve`, { method: 'POST' });
if (response.ok) await refreshData();
else alert('Error approving task');
} catch (error) { alert('Error approving task'); }
}
function editTask(taskId) {
const task = currentData.tasks.find(t => t.id === taskId);
if (task) openTaskModal(task);
}
async function deleteTask(taskId) {
if (!confirm('Are you sure you want to delete this task?')) return;
try {
const response = await fetch(`/api/tasks/${taskId}`, { method: 'DELETE' });
if (response.ok) await refreshData();
else alert('Error deleting task');
} catch (error) { alert('Error deleting task'); }
}
document.getElementById('sprintForm').addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const data = Object.fromEntries(formData);
try {
const response = await fetch('/api/sprints', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (response.ok) {
closeModal('sprintModal');
await fetchCurrentSprint();
} else alert('Error creating sprint');
} catch (error) { alert('Error creating sprint'); }
});
document.getElementById('taskForm').addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const data = Object.fromEntries(formData);
const isEdit = !!data.task_id;
try {
const url = isEdit ? `/api/tasks/${data.task_id}` : '/api/tasks';
const method = isEdit ? 'PUT' : 'POST';
const response = await fetch(url, {
method: method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (response.ok) {
closeModal('taskModal');
await refreshData();
} else alert('Error saving task');
} catch (error) { alert('Error saving task'); }
});
async function refreshData() {
await Promise.all([fetchCurrentSprint(), fetchProgress(), fetchTasks(), fetchEpics()]);
}
setInterval(refreshData, 30000);
refreshData();
window.addEventListener('click', (e) => {
if (e.target.classList.contains('modal')) {
e.target.classList.remove('show');
}
});
</script>
</body>
</html>