UNPKG

bmad-method-mcp

Version:

Breakthrough Method of Agile AI-driven Development with Enhanced MCP Integration

496 lines (468 loc) 25.4 kB
<!DOCTYPE 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')">&times;</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')">&times;</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>