This commit is contained in:
Raven Scott
2025-12-17 20:05:50 -05:00
commit 742e27d3f7
276 changed files with 89838 additions and 0 deletions
@@ -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();