@boundless-oss/atlas
Version:
Atlas - MCP Server for comprehensive startup project management
1,477 lines (1,301 loc) • 164 kB
JavaScript
// 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