reorg
This commit is contained in:
@@ -0,0 +1,369 @@
|
||||
/**
|
||||
* 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 = `
|
||||
<div class="text-center py-12">
|
||||
<div class="spinner"></div>
|
||||
<p class="mt-4 text-tertiary">Loading overview...</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 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 = `
|
||||
<div class="text-center py-12">
|
||||
<p class="text-red-400">Error loading overview: ${error.message}</p>
|
||||
<button onclick="location.reload()" class="btn btn-primary mt-4">
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the content
|
||||
*/
|
||||
renderContent(container) {
|
||||
const { overview, metrics, peers } = this.data;
|
||||
const stats = overview.stats;
|
||||
|
||||
container.innerHTML = `
|
||||
<!-- Key Statistics Cards -->
|
||||
<div class="mb-6">
|
||||
<div class="grid grid-cols-5 gap-4">
|
||||
<div class="glass-card">
|
||||
<h3 class="text-sm text-secondary mb-1">Total Domains</h3>
|
||||
<p class="text-2xl font-bold text-indigo-400">${window.utils.formatNumber(stats.totalDomains)}</p>
|
||||
</div>
|
||||
<div class="glass-card">
|
||||
<h3 class="text-sm text-secondary mb-1">Resolved</h3>
|
||||
<p class="text-2xl font-bold text-green-400">${window.utils.formatNumber(stats.resolved)}</p>
|
||||
</div>
|
||||
<div class="glass-card">
|
||||
<h3 class="text-sm text-secondary mb-1">Insufficient Quorum</h3>
|
||||
<p class="text-2xl font-bold text-yellow-400">${window.utils.formatNumber(stats.insufficientQuorum)}</p>
|
||||
</div>
|
||||
<div class="glass-card">
|
||||
<h3 class="text-sm text-secondary mb-1">Ties</h3>
|
||||
<p class="text-2xl font-bold text-orange-400">${window.utils.formatNumber(stats.tie)}</p>
|
||||
</div>
|
||||
<div class="glass-card">
|
||||
<h3 class="text-sm text-secondary mb-1">Active Peers</h3>
|
||||
<p class="text-2xl font-bold text-blue-400">${window.utils.formatNumber(peers.activePeers)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Metrics Cards -->
|
||||
<div class="mb-6 grid grid-cols-4 gap-4">
|
||||
<div class="glass-card">
|
||||
<h3 class="text-sm text-secondary mb-1">Total Resolutions</h3>
|
||||
<p class="text-2xl font-bold text-purple-400">${window.utils.formatNumber(metrics.metrics?.resolutions || 0)}</p>
|
||||
</div>
|
||||
<div class="glass-card">
|
||||
<h3 class="text-sm text-secondary mb-1">Quorum Failures</h3>
|
||||
<p class="text-2xl font-bold text-yellow-400">${window.utils.formatNumber(metrics.metrics?.quorumFailures || 0)}</p>
|
||||
</div>
|
||||
<div class="glass-card">
|
||||
<h3 class="text-sm text-secondary mb-1">Total Votes</h3>
|
||||
<p class="text-2xl font-bold text-blue-400">${window.utils.formatNumber(metrics.metrics?.totalVotes || 0)}</p>
|
||||
</div>
|
||||
<div class="glass-card">
|
||||
<h3 class="text-sm text-secondary mb-1">Avg Votes/Domain</h3>
|
||||
<p class="text-2xl font-bold text-indigo-400">${(metrics.metrics?.avgVotesPerDomain || 0).toFixed(2)}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Charts Row -->
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<!-- Consensus Status Pie Chart -->
|
||||
<div class="glass-card">
|
||||
<h3 class="text-sm font-semibold text-secondary mb-4">Consensus Status Distribution</h3>
|
||||
<div style="height: 300px; position: relative;">
|
||||
<canvas id="statusChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Consensus Metrics Bar Chart -->
|
||||
<div class="glass-card">
|
||||
<h3 class="text-sm font-semibold text-secondary mb-4">Consensus Metrics</h3>
|
||||
<div style="height: 300px; position: relative;">
|
||||
<canvas id="metricsChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 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();
|
||||
|
||||
Reference in New Issue
Block a user