/** * Overview dashboard view */ class OverviewView { constructor() { this.charts = {}; this.data = null; } /** * Render the overview view */ async render() { const container = document.getElementById('overviewContent'); if (!container) return; // Destroy existing charts before re-rendering this.destroy(); try { // Show loading state container.innerHTML = `

Loading overview...

`; // Fetch data const [overviewData, metricsData, peersData] = await Promise.all([ window.apiClient.getOverview(), window.apiClient.getMetrics(), window.apiClient.getPeers() ]); this.data = { overview: overviewData, metrics: metricsData, peers: peersData }; // Render the view this.renderContent(container); // Update stats in sidebar this.updateSidebarStats(); } catch (error) { console.error('Error loading overview:', error); container.innerHTML = `

Error loading overview: ${error.message}

`; } } /** * Render the content */ renderContent(container) { const { overview, metrics, peers } = this.data; const stats = overview.stats; container.innerHTML = `

Total Domains

${window.utils.formatNumber(stats.totalDomains)}

Resolved

${window.utils.formatNumber(stats.resolved)}

Insufficient Quorum

${window.utils.formatNumber(stats.insufficientQuorum)}

Ties

${window.utils.formatNumber(stats.tie)}

Active Peers

${window.utils.formatNumber(peers.activePeers)}

Total Resolutions

${window.utils.formatNumber(metrics.metrics?.resolutions || 0)}

Quorum Failures

${window.utils.formatNumber(metrics.metrics?.quorumFailures || 0)}

Total Votes

${window.utils.formatNumber(metrics.metrics?.totalVotes || 0)}

Avg Votes/Domain

${(metrics.metrics?.avgVotesPerDomain || 0).toFixed(2)}

Consensus Status Distribution

Consensus Metrics

`; // Render charts after a brief delay to ensure DOM is ready and container is visible setTimeout(() => { const container = document.getElementById('overviewContent'); const statusChart = document.getElementById('statusChart'); const metricsChart = document.getElementById('metricsChart'); // Only render charts if the container is visible and canvas elements exist if (container && container.offsetParent !== null && statusChart && metricsChart) { this.renderCharts(); } else { // Retry after a longer delay if container isn't visible yet setTimeout(() => { const retryStatusChart = document.getElementById('statusChart'); const retryMetricsChart = document.getElementById('metricsChart'); if (retryStatusChart && retryMetricsChart) { this.renderCharts(); } }, 200); } }, 100); } /** * Update existing charts with new data */ updateCharts() { if (!this.data) return; const { overview, metrics } = this.data; const stats = overview.stats; // Update Status Pie Chart if (this.charts.status) { this.charts.status.data.datasets[0].data = [ stats.resolved, stats.insufficientQuorum, stats.tie, stats.noClaims, stats.error ]; this.charts.status.update('none'); // 'none' means no animation for smoother updates } // Update Metrics Bar Chart if (this.charts.metrics) { const metricsData = metrics.metrics || {}; this.charts.metrics.data.datasets[0].data = [ metricsData.resolutions || 0, metricsData.quorumFailures || 0, metricsData.ties || 0, metricsData.validationFailures || 0 ]; this.charts.metrics.update('none'); } } /** * Render charts */ renderCharts() { // Destroy existing charts first to prevent duplicates if (this.charts.status) { this.charts.status.destroy(); this.charts.status = null; } if (this.charts.metrics) { this.charts.metrics.destroy(); this.charts.metrics = null; } const { overview, metrics } = this.data; const stats = overview.stats; // Status Pie Chart const statusCtx = document.getElementById('statusChart'); if (statusCtx && !this.charts.status) { this.charts.status = new Chart(statusCtx, { type: 'pie', data: { labels: ['Resolved', 'Insufficient Quorum', 'Tie', 'No Claims', 'Error'], datasets: [{ data: [ stats.resolved, stats.insufficientQuorum, stats.tie, stats.noClaims, stats.error ], backgroundColor: [ '#10b981', // green '#eab308', // yellow '#f97316', // orange '#6b7280', // gray '#ef4444' // red ] }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom', labels: { color: '#d1d5db' } } } } }); } // Metrics Bar Chart const metricsCtx = document.getElementById('metricsChart'); if (metricsCtx && !this.charts.metrics) { const metricsData = metrics.metrics || {}; this.charts.metrics = new Chart(metricsCtx, { type: 'bar', data: { labels: ['Resolutions', 'Quorum Failures', 'Ties', 'Validation Failures'], datasets: [{ label: 'Count', data: [ metricsData.resolutions || 0, metricsData.quorumFailures || 0, metricsData.ties || 0, metricsData.validationFailures || 0 ], backgroundColor: [ '#10b981', '#eab308', '#f97316', '#ef4444' ] }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { y: { beginAtZero: true, ticks: { color: '#d1d5db' }, grid: { color: '#374151' } }, x: { ticks: { color: '#d1d5db' }, grid: { color: '#374151' } } } } }); } } /** * Update sidebar stats */ updateSidebarStats() { if (this.data && this.data.overview && this.data.peers) { const { overview, peers } = this.data; const stats = overview.stats; const totalDomainsEl = document.getElementById('statTotalDomains'); const resolvedEl = document.getElementById('statResolved'); const activePeersEl = document.getElementById('statActivePeers'); if (totalDomainsEl) totalDomainsEl.textContent = window.utils.formatNumber(stats.totalDomains); if (resolvedEl) resolvedEl.textContent = window.utils.formatNumber(stats.resolved); if (activePeersEl) activePeersEl.textContent = window.utils.formatNumber(peers.activePeers); } else { // Fallback to shared function if data not available window.utils.updateSidebarStats(); } } /** * Handle updates from WebSocket */ async handleUpdate(data) { // Always update on any consensus-related change if (data.overview || data.metrics || data.peers || data.domains || data.changedDomains) { // If charts already exist, just update the data instead of re-rendering if (this.charts.status || this.charts.metrics) { try { // Fetch fresh data const [overviewData, metricsData, peersData] = await Promise.all([ window.apiClient.getOverview(), window.apiClient.getMetrics(), window.apiClient.getPeers() ]); this.data = { overview: overviewData, metrics: metricsData, peers: peersData }; // Update sidebar stats this.updateSidebarStats(); // Update charts without destroying them this.updateCharts(); } catch (error) { console.error('Error updating overview:', error); } } else { // If charts don't exist yet, do a full render window.apiClient.invalidateCache(); await this.render(); } } } /** * Destroy charts */ destroy() { Object.values(this.charts).forEach(chart => { if (chart && chart.destroy) { chart.destroy(); } }); this.charts = {}; } } // Export window.overviewView = new OverviewView();