UNPKG

@boundless-oss/atlas

Version:

Atlas - MCP Server for comprehensive startup project management

1,477 lines (1,301 loc) 164 kB
// Atlas Dashboard JavaScript class AtlasDashboard { constructor() { this.socket = null; this.data = { performance: {}, security: {}, errors: {}, agile: {} }; this.charts = {}; this.autoRefreshInterval = null; this.autoRefreshEnabled = true; this.autoRefreshPeriod = 30000; // 30 seconds default this.init(); } init() { this.setupWebSocket(); this.setupNavigation(); this.setupEventListeners(); this.setupStoryCardListeners(); this.loadInitialData(); // Start auto-refresh this.startAutoRefresh(); } setupWebSocket() { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const host = window.location.host; this.socket = io(`${protocol}//${host}`, { transports: ['websocket', 'polling'] }); this.socket.on('connect', () => { console.log('Connected to Atlas Dashboard'); this.updateConnectionStatus('connected'); }); this.socket.on('disconnect', () => { console.log('Disconnected from Atlas Dashboard'); this.updateConnectionStatus('disconnected'); }); this.socket.on('error', (error) => { console.error('WebSocket error:', error); this.updateConnectionStatus('error'); }); // Handle real-time data updates this.socket.on('performance_update', (data) => { this.handlePerformanceUpdate(data); }); this.socket.on('security_update', (data) => { this.handleSecurityUpdate(data); }); this.socket.on('errors_update', (data) => { this.handleErrorsUpdate(data); }); this.socket.on('story_moved', (data) => { this.handleStoryMoved(data); }); this.socket.on('story_updated', (data) => { this.handleStoryUpdated(data); }); // Handle data creation events this.socket.on('sprint_created', (data) => { this.handleSprintCreated(data); }); this.socket.on('story_created', (data) => { this.handleStoryCreated(data); }); this.socket.on('epic_created', (data) => { this.handleEpicCreated(data); }); // Handle initial data this.socket.on('initial_performance', (data) => { this.data.performance = data; this.updateOverviewMetrics(); this.updatePerformanceSection(); }); this.socket.on('initial_security', (data) => { this.data.security = data; this.updateSecuritySection(); }); this.socket.on('initial_errors', (data) => { this.data.errors = data; this.updateErrorsSection(); }); this.socket.on('initial_agile', (data) => { this.data.agile = data; this.updateAgileSection(); }); } updateConnectionStatus(status) { const statusElement = document.getElementById('connectionStatus'); const icon = statusElement.querySelector('i'); const text = statusElement.querySelector('span'); statusElement.className = `connection-status ${status}`; switch (status) { case 'connected': icon.className = 'fas fa-circle'; text.textContent = 'Connected'; break; case 'disconnected': icon.className = 'fas fa-circle'; text.textContent = 'Disconnected'; break; case 'error': icon.className = 'fas fa-exclamation-circle'; text.textContent = 'Connection Error'; break; default: icon.className = 'fas fa-circle'; text.textContent = 'Connecting...'; } this.updateLastUpdated(); } updateLastUpdated() { const element = document.getElementById('lastUpdated'); element.textContent = `Last updated: ${new Date().toLocaleTimeString()}`; } setupNavigation() { const navButtons = document.querySelectorAll('.nav-btn'); const sections = document.querySelectorAll('.dashboard-section'); navButtons.forEach(btn => { btn.addEventListener('click', () => { const targetSection = btn.dataset.section; // Update nav buttons navButtons.forEach(b => b.classList.remove('active')); btn.classList.add('active'); // Update sections sections.forEach(section => { section.classList.remove('active'); if (section.id === targetSection) { section.classList.add('active'); } }); // Load section-specific data this.loadSectionData(targetSection); }); }); } setupEventListeners() { // Refresh button document.getElementById('refreshBtn').addEventListener('click', () => { this.refreshDashboard(); }); // Auto-refresh toggle document.getElementById('autoRefreshToggle')?.addEventListener('click', () => { this.toggleAutoRefresh(); }); // Refresh interval selector document.getElementById('refreshInterval')?.addEventListener('change', (e) => { const seconds = parseInt(e.target.value); this.setAutoRefreshPeriod(seconds); }); // Time range selectors document.getElementById('performanceTimeRange')?.addEventListener('change', (e) => { this.loadPerformanceData(e.target.value); }); document.getElementById('errorTimeRange')?.addEventListener('change', (e) => { this.loadErrorData(e.target.value); }); // Sprint selector document.getElementById('sprintSelector')?.addEventListener('change', (e) => { this.loadSprintData(e.target.value); }); // Refresh board button document.getElementById('refreshBoard')?.addEventListener('click', () => { this.loadAgileData(); }); // Create Sprint button document.getElementById('createSprintBtn')?.addEventListener('click', () => { this.showCreateSprintForm(); }); // Create Story button document.getElementById('createStoryBtn')?.addEventListener('click', () => { this.showCreateStoryForm(); }); // Create Epic button document.getElementById('createEpicBtn')?.addEventListener('click', () => { this.showCreateEpicForm(); }); // Modal close button document.getElementById('closeModal')?.addEventListener('click', () => { this.closeStoryModal(); }); // Modal backdrop click document.querySelector('.modal-backdrop')?.addEventListener('click', () => { this.closeStoryModal(); }); // ESC key to close modal document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && this.isModalOpen()) { this.closeStoryModal(); } }); // Listen for story updates from the story editor window.addEventListener('storyUpdated', (e) => { console.log('Story updated:', e.detail.storyId); // Refresh the agile board to show updated data this.loadAgileData(); // If story modal is open, refresh it if (this.isModalOpen() && this.currentStoryId === e.detail.storyId) { this.openStoryModal(e.detail.storyId); } }); } setupStoryCardListeners() { // Use event delegation for dynamically created story cards document.addEventListener('click', (e) => { const storyCard = e.target.closest('.story-card'); if (storyCard && !e.target.closest('.story-tag')) { e.preventDefault(); const storyId = storyCard.dataset.storyId; this.openStoryModal(storyId); } }); } async loadInitialData() { try { // Load health status const healthResponse = await fetch('/api/health'); const healthData = await healthResponse.json(); this.updateSystemHealth(healthData); // Load overview data await this.loadOverviewData(); } catch (error) { console.error('Error loading initial data:', error); this.showError('Failed to load initial dashboard data'); } } async loadOverviewData() { try { // Load performance overview const perfResponse = await fetch('/api/metrics/overview'); if (perfResponse.ok) { const perfData = await perfResponse.json(); this.data.performance = perfData.data; } // Load security overview const secResponse = await fetch('/api/security/status'); if (secResponse.ok) { const secData = await secResponse.json(); this.data.security = secData.data; } // Load error statistics const errorResponse = await fetch('/api/errors/stats'); if (errorResponse.ok) { const errorData = await errorResponse.json(); this.data.errors = errorData.data; } // Load agile overview const agileResponse = await fetch('/api/agile/sprints'); if (agileResponse.ok) { const agileData = await agileResponse.json(); this.data.agile = agileData.data; } this.updateOverviewMetrics(); this.loadActivityFeed(); } catch (error) { console.error('Error loading overview data:', error); } } async updateOverviewMetrics() { const performance = this.data.performance; const security = this.data.security; const errors = this.data.errors; const agile = this.data.agile; // Performance metric if (performance && performance.overallSuccessRate !== undefined) { document.getElementById('overviewPerformance').textContent = `${(performance.overallSuccessRate * 100).toFixed(1)}%`; const trend = performance.overallSuccessRate >= 0.9 ? 'positive' : performance.overallSuccessRate >= 0.8 ? 'stable' : 'negative'; this.updateTrend('performanceTrend', trend); } // Security metric if (security && security.overall?.status) { const securityScore = security.overall.status === 'healthy' ? 100 : security.overall.status === 'attention_required' ? 75 : 50; document.getElementById('overviewSecurity').textContent = securityScore; const trend = security.overall.status === 'healthy' ? 'positive' : security.overall.status === 'attention_required' ? 'stable' : 'negative'; this.updateTrend('securityTrend', trend); } // Errors metric if (errors.totalErrors !== undefined) { document.getElementById('overviewErrors').textContent = errors.totalErrors; this.updateTrend('errorsTrend', errors.trends?.trend || 'stable'); } // Sprint metric if (agile && agile.sprints && agile.sprints.length > 0) { const activeSprint = agile.sprints.find(s => s.status === 'active'); if (activeSprint && activeSprint.storiesCompleted !== undefined && activeSprint.storiesTotal !== undefined) { const progress = `${activeSprint.storiesCompleted}/${activeSprint.storiesTotal}`; document.getElementById('overviewSprint').textContent = progress; const completionRate = activeSprint.storiesTotal > 0 ? (activeSprint.storiesCompleted / activeSprint.storiesTotal) * 100 : 0; const trend = completionRate >= 75 ? 'positive' : completionRate >= 50 ? 'stable' : 'negative'; this.updateTrend('sprintTrend', trend); } else { // No active sprint or missing data document.getElementById('overviewSprint').textContent = '0/0'; this.updateTrend('sprintTrend', 'stable'); } } else { // No sprint data available document.getElementById('overviewSprint').textContent = '--'; this.updateTrend('sprintTrend', 'stable'); } // Load Phase 2 Analytics data this.loadPhase2Analytics(); } updateTrend(elementId, trend) { const element = document.getElementById(elementId); element.className = `metric-trend ${trend}`; const icons = { positive: 'fas fa-arrow-up', negative: 'fas fa-arrow-down', stable: 'fas fa-minus', increasing: 'fas fa-arrow-up', decreasing: 'fas fa-arrow-down' }; const texts = { positive: 'Improving', negative: 'Declining', stable: 'Stable', increasing: 'Increasing', decreasing: 'Decreasing' }; element.innerHTML = ` <i class="${icons[trend] || 'fas fa-minus'}"></i> ${texts[trend] || 'Stable'} `; } updateSystemHealth(healthData) { const element = document.getElementById('systemHealth'); const icon = element.querySelector('i'); const text = element.querySelector('span'); if (healthData.success) { element.className = 'system-health healthy'; icon.className = 'fas fa-heart'; text.textContent = 'System Healthy'; } else { element.className = 'system-health error'; icon.className = 'fas fa-exclamation-triangle'; text.textContent = 'System Issues'; } } async loadActivityFeed() { const feedElement = document.getElementById('activityFeed'); try { // Combine recent activities from different sources const activities = []; // Add performance activities if (this.data.performance.recentExecutions) { this.data.performance.recentExecutions.slice(0, 3).forEach(exec => { activities.push({ type: 'performance', icon: 'fas fa-tachometer-alt', text: `Tool "${exec.toolName}" executed successfully`, time: exec.timestamp, severity: exec.success ? 'success' : 'error' }); }); } // Add security activities if (this.data.security.recentEvents && Array.isArray(this.data.security.recentEvents)) { this.data.security.recentEvents.slice(0, 2).forEach(event => { activities.push({ type: 'security', icon: 'fas fa-shield-alt', text: event.description, time: event.timestamp, severity: event.severity }); }); } // Add error activities if (this.data.errors.recentErrors) { this.data.errors.recentErrors.slice(0, 2).forEach(error => { activities.push({ type: 'error', icon: 'fas fa-bug', text: `Error in ${error.tool}: ${error.message}`, time: error.timestamp, severity: error.severity }); }); } // Sort by time and limit to recent items activities.sort((a, b) => new Date(b.time) - new Date(a.time)); const recentActivities = activities.slice(0, 8); // Render activities if (recentActivities.length === 0) { feedElement.innerHTML = ` <div class="activity-item"> <i class="fas fa-info-circle"></i> <span>No recent activity</span> </div> `; } else { feedElement.innerHTML = recentActivities.map(activity => ` <div class="activity-item"> <i class="${activity.icon} ${activity.severity}"></i> <div class="activity-content"> <div class="activity-text">${activity.text}</div> <div class="activity-time">${this.formatTime(activity.time)}</div> </div> </div> `).join(''); } } catch (error) { console.error('Error loading activity feed:', error); feedElement.innerHTML = ` <div class="activity-item"> <i class="fas fa-exclamation-triangle"></i> <span>Failed to load recent activity</span> </div> `; } } loadSectionData(section) { // Show loading indicator console.log(`Loading data for ${section} section`); switch (section) { case 'overview': this.loadOverviewData().catch(err => { console.error('Failed to load overview data:', err); this.showError('Failed to load overview data'); }); break; case 'performance': this.loadPerformanceData().catch(err => { console.error('Failed to load performance data:', err); this.showError('Failed to load performance data'); }); break; case 'agile': this.loadAgileData().catch(err => { console.error('Failed to load agile data:', err); this.showError('Failed to load agile data'); }); break; case 'epics': this.loadEpicsData().catch(err => { console.error('Failed to load epics data:', err); this.showError('Failed to load epics data'); }); break; case 'timeline': this.loadTimelineData().catch(err => { console.error('Failed to load timeline data:', err); this.showError('Failed to load timeline data'); }); break; case 'security': this.loadSecurityData().catch(err => { console.error('Failed to load security data:', err); this.showError('Failed to load security data'); }); break; case 'errors': this.loadErrorData().catch(err => { console.error('Failed to load error data:', err); this.showError('Failed to load error data'); }); break; default: console.warn(`Unknown section: ${section}`); } } async loadPerformanceData(timeRange = '24h') { try { // Load system metrics const response = await fetch(`/api/metrics/overview?timeRange=${timeRange}`); if (!response.ok) throw new Error('Failed to load performance data'); const data = await response.json(); this.data.performance = data.data; // Also load quality metrics try { const qualityResponse = await fetch(`/api/metrics/quality?timeRange=${timeRange}`); if (qualityResponse.ok) { const qualityData = await qualityResponse.json(); this.data.performance.qualityMetrics = qualityData.data.qualityMetrics; } } catch (qualityError) { console.warn('Quality metrics not available:', qualityError); } this.updatePerformanceSection(); } catch (error) { console.error('Error loading performance data:', error); this.showError('Failed to load performance data'); } } updatePerformanceSection() { // Update performance charts if (window.updatePerformanceCharts) { window.updatePerformanceCharts(this.data.performance); } // Update performance alerts this.loadPerformanceAlerts(); } async loadPerformanceAlerts() { try { const response = await fetch('/api/metrics/alerts'); if (!response.ok) return; const data = await response.json(); const alertsContainer = document.getElementById('performanceAlerts'); if (data.data.alerts.length === 0) { alertsContainer.innerHTML = ` <div class="alert-item"> <i class="fas fa-check-circle"></i> <span>No performance alerts</span> </div> `; } else { alertsContainer.innerHTML = data.data.alerts.map(alert => ` <div class="alert-item ${alert.severity}"> <i class="fas fa-exclamation-triangle"></i> <div class="alert-content"> <div class="alert-title">${alert.title}</div> <div class="alert-message">${alert.message}</div> ${alert.suggestion ? `<div class="alert-suggestion">${alert.suggestion}</div>` : ''} </div> </div> `).join(''); } } catch (error) { console.error('Error loading performance alerts:', error); } } async loadAgileData() { try { console.log('Loading agile data...'); // Load sprints for selector let hasActiveSprint = false; const sprintsResponse = await fetch('/api/agile/sprints'); if (sprintsResponse.ok) { const sprintsData = await sprintsResponse.json(); const sprints = sprintsData.data.sprints || []; console.log('Loaded', sprints.length, 'sprints'); if (sprints.length > 0) { this.updateSprintSelector(sprints); // Load active sprint by default const activeSprint = sprints.find(s => s.status === 'active'); if (activeSprint) { console.log('Found active sprint:', activeSprint); console.log('Loading active sprint with ID:', activeSprint.id); await this.loadSprintData(activeSprint.id); hasActiveSprint = true; } else { console.log('No active sprint found, loading all stories'); // Clear the loading message when no active sprint await this.loadSprintData(''); } } else { // No sprints available - show message and load all stories console.log('No sprints available, showing backlog mode'); this.updateSprintSelector([]); document.getElementById('sprintInfo').innerHTML = ` <div class="no-sprint-message"> <h3>No Sprints Available</h3> <p>Showing all stories in backlog mode</p> </div> `; } } else { console.error('Failed to load sprints:', sprintsResponse.status); document.getElementById('sprintInfo').innerHTML = ` <div class="error-message"> <h3>Failed to Load Sprints</h3> <p>Unable to connect to the server</p> </div> `; } // Always load all stories if no active sprint was found if (!hasActiveSprint) { await this.loadAllStories(); } } catch (error) { console.error('Error loading agile data:', error); document.getElementById('sprintInfo').innerHTML = ` <div class="error-message"> <h3>Error Loading Data</h3> <p>${error.message}</p> </div> `; this.showError('Failed to load agile data'); } } updateSprintSelector(sprints) { const selector = document.getElementById('sprintSelector'); if (!selector) { console.error('Sprint selector element not found!'); return; } if (sprints.length === 0) { selector.innerHTML = '<option value="">No sprints available - Showing all stories</option>'; selector.disabled = true; } else { selector.disabled = false; // Add "All Sprints" option first let options = '<option value="">All Sprints</option>'; // Add individual sprint options options += sprints.map(sprint => ` <option value="${sprint.id}" ${sprint.status === 'active' ? 'selected' : ''}> ${sprint.name} (${sprint.status}) </option> `).join(''); selector.innerHTML = options; // Ensure the active sprint is selected const activeSprint = sprints.find(s => s.status === 'active'); if (activeSprint) { console.log('Setting sprint selector to active sprint:', activeSprint.id); selector.value = activeSprint.id; } } } async loadSprintData(sprintId) { try { if (!sprintId) { // Load all stories when "All Sprints" is selected const storiesResponse = await fetch('/api/agile/stories'); if (storiesResponse.ok) { const storiesData = await storiesResponse.json(); this.updateKanbanBoard(storiesData.data.stories); } // Show aggregate sprint info this.showAllSprintsInfo(); } else { // Load sprint details console.log(`Fetching sprint details for ID: ${sprintId}`); try { const sprintResponse = await fetch(`/api/agile/sprints/${sprintId}`); console.log('Sprint response status:', sprintResponse.status); if (sprintResponse.ok) { const sprintData = await sprintResponse.json(); console.log('Sprint data received:', sprintData); if (sprintData.success && sprintData.data) { console.log('Updating sprint info with data:', sprintData.data); this.updateSprintInfo(sprintData.data); } else { console.error('Invalid sprint data structure:', sprintData); // Clear loading message and show error document.getElementById('sprintInfo').innerHTML = ` <div class="error-message"> <h3>Error Loading Sprint</h3> <p>Invalid sprint data received from server</p> </div> `; } } else { console.error('Failed to fetch sprint:', sprintResponse.status, sprintResponse.statusText); // Clear loading message and show error document.getElementById('sprintInfo').innerHTML = ` <div class="error-message"> <h3>Error Loading Sprint</h3> <p>Failed to fetch sprint details (${sprintResponse.status})</p> </div> `; } } catch (sprintError) { console.error('Error fetching sprint details:', sprintError); document.getElementById('sprintInfo').innerHTML = ` <div class="error-message"> <h3>Error Loading Sprint</h3> <p>${sprintError.message}</p> </div> `; } // Load stories for the sprint try { const storiesResponse = await fetch(`/api/agile/stories?sprintId=${sprintId}`); if (storiesResponse.ok) { const storiesData = await storiesResponse.json(); this.updateKanbanBoard(storiesData.data.stories); } } catch (storiesError) { console.error('Error loading sprint stories:', storiesError); } // Load burndown chart this.loadBurndownChart(sprintId); } // Load velocity chart (works for both cases) this.loadVelocityChart(); } catch (error) { console.error('Error loading sprint data:', error); // Ensure loading message is cleared on error const sprintInfo = document.getElementById('sprintInfo'); if (sprintInfo && sprintInfo.innerHTML.includes('Loading sprint information')) { sprintInfo.innerHTML = ` <div class="error-message"> <h3>Error Loading Sprint Data</h3> <p>${error.message}</p> </div> `; } } } updateSprintInfo(sprint) { console.log('updateSprintInfo called with sprint:', sprint); const sprintInfoElement = document.getElementById('sprintInfo'); if (!sprintInfoElement) { console.error('Sprint info element not found!'); return; } // Validate required sprint fields if (!sprint || !sprint.name) { console.error('Invalid sprint data - missing required fields:', sprint); sprintInfoElement.innerHTML = ` <div class="error-message"> <h3>Invalid Sprint Data</h3> <p>Sprint data is missing required fields</p> </div> `; return; } // Recalculate progress based on actual current stories if (this.currentStories && this.currentStories.length > 0) { const totalPoints = this.currentStories.reduce((sum, story) => sum + (story.storyPoints || 0), 0); const completedPoints = this.currentStories .filter(story => story.status === 'done') .reduce((sum, story) => sum + (story.storyPoints || 0), 0); // Update sprint with recalculated values sprint.storyPointsPlanned = totalPoints; sprint.storyPointsCompleted = completedPoints; sprint.storiesTotal = this.currentStories.length; sprint.storiesCompleted = this.currentStories.filter(s => s.status === 'done').length; sprint.velocity = completedPoints; console.log('Recalculated sprint progress:', { totalPoints, completedPoints, storiesTotal: sprint.storiesTotal, storiesCompleted: sprint.storiesCompleted }); } const progress = sprint.storyPointsPlanned > 0 ? (sprint.storyPointsCompleted / sprint.storyPointsPlanned * 100) : 0; const daysLeft = this.calculateDaysLeft(sprint.endDate); const progressColor = progress >= 80 ? 'var(--md-success)' : progress >= 50 ? 'var(--md-warning)' : 'var(--md-error)'; console.log('Updating sprint info HTML for sprint:', sprint.name); sprintInfoElement.innerHTML = ` <div class="sprint-details"> <div class="sprint-header"> <h3><i class="fas fa-rocket"></i> ${sprint.name}</h3> <span class="sprint-status ${sprint.status}">${sprint.status}</span> </div> <div class="sprint-meta"> <span><i class="fas fa-bullseye"></i> <strong>Goal:</strong> ${sprint.goal}</span> <span><i class="fas fa-calendar-alt"></i> <strong>Duration:</strong> ${this.formatDateRange(sprint.startDate, sprint.endDate)}</span> <span><i class="fas fa-clock"></i> <strong>Days Left:</strong> ${daysLeft > 0 ? daysLeft : 0} days</span> <span><i class="fas fa-users"></i> <strong>Team Size:</strong> ${sprint.team ? sprint.team.length : 0} members</span> </div> <div class="sprint-progress"> <div class="sprint-progress-header"> <span><strong>${progress.toFixed(1)}%</strong> Complete</span> <span>${sprint.storyPointsCompleted || 0} / ${sprint.storyPointsPlanned || 0} points</span> </div> <div class="progress-bar"> <div class="progress-fill" style="width: ${progress}%; background: ${progressColor}"></div> </div> </div> <div class="sprint-metrics"> <div class="metric-card mini"> <i class="fas fa-check-circle"></i> <div class="metric-content"> <div class="metric-value">${sprint.storiesCompleted}</div> <div class="metric-label">Completed</div> </div> </div> <div class="metric-card mini"> <i class="fas fa-tasks"></i> <div class="metric-content"> <div class="metric-value">${sprint.storiesTotal - sprint.storiesCompleted}</div> <div class="metric-label">Remaining</div> </div> </div> <div class="metric-card mini"> <i class="fas fa-tachometer-alt"></i> <div class="metric-content"> <div class="metric-value">${sprint.velocity || 0}</div> <div class="metric-label">Velocity</div> </div> </div> <div class="metric-card mini"> <i class="fas fa-fire"></i> <div class="metric-content"> <div class="metric-value">${Math.round((sprint.storyPointsCompleted || 0) / (sprint.duration || 14) * 7)}</div> <div class="metric-label">Weekly Rate</div> </div> </div> </div> </div> `; } calculateDaysLeft(endDate) { const end = new Date(endDate); const now = new Date(); const diffTime = end - now; const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); return diffDays; } updateKanbanBoard(stories) { // Store stories for sprint progress calculation this.currentStories = stories; // Group stories by status const storyGroups = { 'backlog': [], 'todo': [], 'in-progress': [], 'done': [] }; stories.forEach(story => { let status = story.status || 'todo'; // Map backend status to frontend status if (status === 'in_progress') status = 'in-progress'; if (status === 'review' || status === 'testing') status = 'in-progress'; // Map review/testing to in-progress if (status === 'blocked' || status === 'cancelled') status = 'todo'; // Map blocked/cancelled to todo if (status === 'blocked') status = 'in-progress'; // Map blocked to in-progress with visual indicator if (storyGroups[status]) { storyGroups[status].push(story); } }); // Calculate totals for each column const columnTotals = {}; Object.keys(storyGroups).forEach(status => { const statusStories = storyGroups[status]; columnTotals[status] = { count: statusStories.length, points: statusStories.reduce((sum, story) => sum + (story.storyPoints || 0), 0) }; }); // Update each column Object.keys(storyGroups).forEach(status => { const containerId = status === 'in-progress' ? 'inProgressCards' : `${status}Cards`; const container = document.getElementById(containerId); const headerElement = container.parentElement.querySelector('.column-header'); // Get the column title const columnTitle = headerElement.querySelector('h3').textContent; // Update header to show both count and points headerElement.innerHTML = ` <h3>${columnTitle}</h3> <div class="column-metrics"> <span class="story-count">${columnTotals[status].count}</span> <span class="story-points">${columnTotals[status].points} pts</span> </div> `; if (storyGroups[status].length === 0) { container.innerHTML = ` <div class="empty-state"> <div class="empty-state-icon"> <i class="fas fa-inbox"></i> </div> <div class="empty-state-title">No stories</div> <div class="empty-state-message">Drag stories here to update their status</div> </div> `; } else { container.innerHTML = storyGroups[status].map(story => this.createStoryCard(story) ).join(''); } }); // Setup drag and drop if available if (window.setupDragAndDrop) { window.setupDragAndDrop(); } } async loadAllStories() { try { // Load all stories without sprint filter const storiesResponse = await fetch('/api/agile/stories?limit=100'); if (storiesResponse.ok) { const storiesData = await storiesResponse.json(); const stories = storiesData.data.stories || []; console.log('Loaded', stories.length, 'stories for backlog view'); this.updateKanbanBoard(stories); // Update backlog summary this.updateBacklogSummary(stories); } else { console.error('Failed to load stories:', storiesResponse.status); this.showError('Failed to load stories'); } } catch (error) { console.error('Error loading all stories:', error); this.showError('Failed to load stories'); } } updateBacklogSummary(stories) { // Count stories by priority and status const summary = { total: stories.length, byPriority: { low: 0, medium: 0, high: 0, critical: 0 }, byStatus: { planning: 0, todo: 0, 'in-progress': 0, review: 0, done: 0, backlog: 0 } }; stories.forEach(story => { summary.byPriority[story.priority] = (summary.byPriority[story.priority] || 0) + 1; summary.byStatus[story.status] = (summary.byStatus[story.status] || 0) + 1; }); // Update sprint info section with backlog summary document.getElementById('sprintInfo').innerHTML += ` <div class="backlog-summary"> <h4>Backlog Summary</h4> <div class="summary-stats"> <span><strong>Total Stories:</strong> ${summary.total}</span> <span><strong>High Priority:</strong> ${summary.byPriority.high || 0}</span> <span><strong>In Progress:</strong> ${summary.byStatus['in-progress'] || 0}</span> <span><strong>Completed:</strong> ${summary.byStatus.done || 0}</span> </div> </div> `; } createStoryCard(story) { const initials = story.assignee ? story.assignee.split(' ').map(n => n[0]).join('').toUpperCase() : 'UN'; // Generate color based on assignee name for consistent avatar colors const colorIndex = story.assignee ? (story.assignee.charCodeAt(0) + story.assignee.charCodeAt(1)) % 10 + 1 : 1; return ` <div class="story-card ${story.status === 'blocked' ? 'blocked' : ''}" data-story-id="${story.id}" data-priority="${story.priority || 'medium'}" data-status="${story.status}" draggable="true" onclick="window.dashboard.openStoryModal('${story.id}')"> <div class="story-header"> <span class="story-id">${story.id}</span> ${story.status === 'blocked' ? '<span class="blocked-indicator" title="Blocked"><i class="fas fa-ban"></i></span>' : ''} <!-- Edit button removed - now handled by unified modal --> <span class="story-points">${story.storyPoints || 0}</span> </div> <div class="story-title" title="${story.title}">${story.title}</div> <div class="story-assignee"> <div class="avatar" data-color="${colorIndex}">${initials}</div> <span>${story.assignee || 'Unassigned'}</span> </div> ${story.tags && story.tags.length > 0 ? ` <div class="story-tags"> ${story.tags.map(tag => `<span class="story-tag">${tag}</span>`).join('')} </div> ` : ''} ${story.epic ? ` <div class="story-epic"> <i class="fas fa-layer-group"></i> <span class="epic-id">${story.epic}</span> </div> ` : ''} <div class="story-progress"> <div class="progress-bar"> <div class="progress-fill" style="width: ${story.progress || 0}%"></div> </div> </div> </div> `; } async loadBurndownChart(sprintId) { try { const response = await fetch(`/api/agile/burndown/${sprintId}`); if (!response.ok) return; const data = await response.json(); // The API now returns Chart.js-ready format, use it directly const canvas = document.getElementById('burndownChart'); if (canvas && data.data) { // Destroy existing chart if (this.charts.burndown) { this.charts.burndown.destroy(); } // Create new chart with API data this.charts.burndown = new Chart(canvas, { type: 'line', data: { labels: data.data.labels, datasets: data.data.datasets }, options: { responsive: true, maintainAspectRatio: false, plugins: { title: { display: true, text: 'Sprint Burndown Chart' }, legend: { display: true, position: 'top' } }, scales: { y: { beginAtZero: true, title: { display: true, text: 'Story Points Remaining' } }, x: { title: { display: true, text: 'Sprint Progress' } } } } }); } } catch (error) { console.error('Error loading burndown chart:', error); } } async loadVelocityChart() { try { const response = await fetch('/api/agile/velocity'); if (!response.ok) return; const data = await response.json(); // The API now returns Chart.js-ready format, use it directly const canvas = document.getElementById('velocityChart'); if (canvas && data.data) { // Destroy existing chart if (this.charts.velocity) { this.charts.velocity.destroy(); } // Create new chart with API data this.charts.velocity = new Chart(canvas, { type: 'bar', data: { labels: data.data.labels, datasets: data.data.datasets }, options: { responsive: true, maintainAspectRatio: false, plugins: { title: { display: true, text: 'Team Velocity Trends' }, legend: { display: true, position: 'top' } }, scales: { y: { beginAtZero: true, title: { display: true, text: 'Story Points Completed' } }, x: { title: { display: true, text: 'Sprint' } } } } }); } } catch (error) { console.error('Error loading velocity chart:', error); } } updateAgileSection() { // This will be called when real-time agile updates are received this.loadAgileData(); } async loadSecurityData() { try { // Load security status const statusResponse = await fetch('/api/security/status'); if (statusResponse.ok) { const statusData = await statusResponse.json(); this.updateSecurityMetrics(statusData.data); } // Load security alerts const alertsResponse = await fetch('/api/security/alerts'); if (alertsResponse.ok) { const alertsData = await alertsResponse.json(); this.updateSecurityAlerts(alertsData.data.alerts); } // Load approval requests const approvalsResponse = await fetch('/api/security/approvals'); if (approvalsResponse.ok) { const approvalsData = await approvalsResponse.json(); this.updateApprovalsList(approvalsData.data.approvals); } } catch (error) { console.error('Error loading security data:', error); } } updateSecurityMetrics(securityData) { const { overall, recentEvents, policies } = securityData; document.getElementById('securityScore').textContent = overall?.securityScore?.toFixed(1) || '--'; document.getElementById('recentEvents').textContent = recentEvents?.total || '--'; document.getElementById('pendingApprovals').textContent = overall?.pendingApprovals || '--'; // Update security status const statusElement = document.getElementById('securityStatus'); const icon = statusElement.querySelector('i'); const text = statusElement.querySelector('span'); if (overall?.status === 'healthy') { statusElement.className = 'security-status healthy'; icon.className = 'fas fa-shield-alt'; text.textContent = 'Security Status: Healthy'; } else { statusElement.className = 'security-status warning'; icon.className = 'fas fa-exclamation-triangle'; text.textContent = 'Security Status: Needs Attention'; } } updateSecurityAlerts(alerts) { const alertsContainer = document.getElementById('securityAlerts'); if (alerts.length === 0) { alertsContainer.innerHTML = ` <div class="alert-item"> <i class="fas fa-check-circle"></i> <span>No security alerts</span> </div> `; } else { alertsContainer.innerHTML = alerts.map(alert => ` <div class="alert-item ${alert.severity}"> <i class="fas fa-exclamation-triangle"></i> <div class="alert-content"> <div class="alert-title">${alert.title}</div> <div class="alert-message">${alert.message}</div> <div class="alert-time">${this.formatTime(alert.timestamp)}</div> </div> </div> `).join(''); } } updateApprovalsList(approvals) { const approvalsContainer = document.getElementById('approvalsList'); if (approvals.length === 0) { approvalsContainer.innerHTML = ` <div class="approval-item"> <i class="fas fa-check-circle"></i> <span>No pending approvals</span> </div> `; } else { approvalsContainer.innerHTML = approvals.map(approval => ` <div class="approval-item"> <div class="approval-header"> <span class="approval-title">${approval.operation}</span> <span class="approval-risk ${approval.riskLevel}">${approval.riskLevel} risk</span> </div> <div class="approval-details"> ${approval.reason} <br> <small>Requested: ${this.formatTime(approval.timestamp)}</small> </div> <div class="approval-actions"> <button class="btn btn-approve" onclick="dashboard.processApproval('${approval.id}', 'approve')"> Approve </button> <button class="btn btn-deny" onclick="dashboard.processApproval('${approval.id}', 'deny')"> Deny </button> </div> </div> `).join(''); } } async processApproval(approvalId, decision) { try { const reason = prompt(`Please provide a reason for ${decision}ing this request:`); if (!reason) return; const response = await fetch(`/api/security/approval/${approvalId}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ decision, reason }) }); if (response.ok) { this.showSuccess(`Approval request ${decision}d successfully`); this.loadSecurityData(); // Refresh the data } else { this.showError(`Failed to ${decision} approval request`); } } catch (error) { console.error(`Error ${decision}ing approval:`, error); this.showError(`Failed to ${decision} approval request`); } } updateSecuritySection() { this.loadSecurityData(); } async loadErrorData(timeRange = '24h') { try { // Load error statistics const statsResponse = await fetch(`/api/errors/stats?timeRange=${timeRange}`); if (statsResponse.ok) { const statsData = await statsResponse.json(); this.updateErrorStats(statsData.data); } // Load error timeline const timelineResponse = await fetch(`/api/errors/timeline?timeRange=${timeRange}`); if (timelineResponse.ok) { const timelineData = await timelineResponse.json(); this.updateErrorTimeline(timelineData.data.timeline); } // Load error trends for charts const trendsResponse = await fetch(`/api/errors/trends?timeRange=${timeRange}`); if (trendsResponse.ok) { const trendsData = await trendsResponse.json(); if (window.updateErrorCharts) { window.updateErrorCharts(trendsData.data); } } } catch (error) { console.error('Error loading error data:', error); } } updateErrorStats(stats) { document.getElementById('totalErrors').textContent = stats.totalErrors || 0; document.getElementById('errorRate').textContent = `${stats.errorRate || 0}/hr`; document.getElementById('criticalErrors').textContent = stats.criticalErrors || 0; document.getElementById('errorPatterns').textContent = stats.patternsFound || 0; } updateErrorTimeline(timeline) { const timelineContainer = document.getElementById('errorTimeline'); if (timeline.length === 0) { timelineContainer.innerHTML = ` <div class="timeline-item"> <i class="fas fa-check-circle"></i> <span>No errors in the selected time range</span> </div> `; } else { timelineContainer.innerHTML = timeline.map(error => ` <div class="timeline-item"> <div class="error-severity ${error.severity}"></div> <div class="error-details"> <div class="error-title">${error.message}</div> <div class="error-meta"> ${error.tool || 'Unknown tool'} • ${error.severity} • ${this.formatTime(error.timestamp)} </div> </div> </div> `).join(''); } } updateErrorsSection() { this.loadErrorData(); } // Real-time update handlers handlePerformanceUpdate(data) { this.data.performance = { ...this.data.performance, ...data.metrics }; this.updateOverviewMetrics(); if (document.querySelector('#performance.active')) { this.updatePerformanceSection(); } this.updateLastUpdated(); } handleSecurityUpdate(data) { this.data.security = { ...this.data.security, ...data.status }; this.updateOverviewMetrics(); if (document.querySelector('#security.active')) { this.updateSecuritySection(); } this.updateLastUpdated(); } handleErrorsUpdate(data) { this.data.errors = { ...this.data.errors, ...data.patterns }; this.updateOverviewMetrics(); if (document.querySelector('#errors.active')) { this.updateErrorsSection(); } this.updateLastUpdated(); } handleStoryMoved(data) { // Update the kanban board in real-time if (document.querySelector('#agile.active')) { this.moveStoryCard(data.storyId, data.fromColumn, data.toColumn); } this.updateLastUpdated(); } handleStoryUpdated(data) { // Update story card in real-time if (document.querySelector('#agile.active')) { this.updateStoryCard(data.storyId, data.updates); } this.updateLastUpdated(); } moveStoryCard(storyId, fromColumn, toColumn) { const storyCard = document.querySelector(`[data-story-id="${storyId}"]`); if (!storyCard) return; const fromContainerId = fromColumn === 'in-progress' ? 'inProgressCards' : `${fromColumn}Cards`; const toContainerId = toColumn === 'in-progress' ? 'inProgressCards' : `${toColumn}Cards`; const fromContainer = document.getElementById(fromContainerId); const toContainer = document.getElementById(toContainerId); if (fromContainer && toContainer) { toContainer.appendChild(storyCard); // Update story counts this.updateColumnCounts(); } } updateStoryCard(storyId, updates) { const storyCard = document.querySelector(`[data-story-id="${storyId}"]`); if (!storyCard) return; // Update story card content based on updates // This would need