Files
meshcore-bot/modules/web_viewer/templates/stats.html
T

356 lines
12 KiB
HTML

{% extends "base.html" %}
{% block title %}Statistics - MeshCore Bot Data Viewer{% endblock %}
{% block content %}
<div class="row">
<div class="col-12">
<h1 class="mb-4">
<i class="fas fa-chart-bar"></i> Statistics
</h1>
</div>
</div>
<!-- Key Metrics -->
<div class="row mb-4">
<div class="col-md-3">
<div class="card">
<div class="card-header">
<i class="fas fa-users"></i> Total Contacts
</div>
<div class="card-body">
<h3 id="total-contacts">0</h3>
<small class="text-muted">All time</small>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card">
<div class="card-header">
<i class="fas fa-comments"></i> Total Adverts
</div>
<div class="card-body">
<h3 id="total-adverts">0</h3>
<small class="text-muted">All time</small>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card">
<div class="card-header">
<i class="fas fa-clock"></i> Recent Activity
</div>
<div class="card-body">
<h3 id="recent-activity">0</h3>
<small class="text-muted">Last 24 hours</small>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card">
<div class="card-header">
<i class="fas fa-chart-line"></i> Growth Rate
</div>
<div class="card-body">
<h3 id="growth-rate">0%</h3>
<small class="text-muted">Daily growth</small>
</div>
</div>
</div>
</div>
<!-- Activity Charts -->
<div class="row mb-4">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<i class="fas fa-chart-pie"></i> Activity Distribution
</div>
<div class="card-body">
<div id="activity-chart">
<div class="loading">Loading activity chart...</div>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">
<i class="fas fa-chart-line"></i> Usage Trends
</div>
<div class="card-body">
<div id="trends-chart">
<div class="loading">Loading trends chart...</div>
</div>
</div>
</div>
</div>
</div>
<!-- Detailed Statistics -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<i class="fas fa-table"></i> Detailed Statistics
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<h6>Contact Statistics</h6>
<ul class="list-unstyled" id="contact-stats">
<li class="loading">Loading contact statistics...</li>
</ul>
</div>
<div class="col-md-6">
<h6>Advert Statistics</h6>
<ul class="list-unstyled" id="advert-stats">
<li class="loading">Loading advert statistics...</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- System Performance -->
<div class="row">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<i class="fas fa-server"></i> System Performance
</div>
<div class="card-body">
<div id="system-performance">
<div class="loading">Loading system performance...</div>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">
<i class="fas fa-cog"></i> Configuration
</div>
<div class="card-body">
<div id="system-config">
<div class="loading">Loading system configuration...</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script nonce="{{ g.csp_nonce }}">
class ModernStatsManager {
constructor() {
this.statsData = {};
this.initializeStats();
}
async initializeStats() {
await this.loadStatsData();
this.setupEventHandlers();
this.updateStatistics();
this.renderCharts();
this.renderDetailedStats();
this.renderSystemInfo();
}
async loadStatsData() {
try {
const response = await fetch('/api/stats');
const data = await response.json();
if (data.error) {
this.showError('Failed to load statistics: ' + data.error);
return;
}
this.statsData = data;
} catch (error) {
console.error('Error loading statistics:', error);
this.showError('Failed to load statistics: ' + error.message);
}
}
setupEventHandlers() {
// Auto-refresh every 30 seconds
setInterval(() => {
this.loadStatsData().then(() => {
this.updateStatistics();
this.renderCharts();
this.renderDetailedStats();
this.renderSystemInfo();
});
}, 30000);
}
updateStatistics() {
document.getElementById('total-contacts').textContent = this.statsData.contact_count || 0;
document.getElementById('total-adverts').textContent = this.statsData.total_advertisements || 0;
document.getElementById('recent-activity').textContent = this.statsData.recent_contacts_24h || 0;
// Calculate growth rate
const recentContacts = this.statsData.recent_contacts_24h || 0;
const totalContacts = this.statsData.contact_count || 0;
const growthRate = totalContacts > 0 ? Math.round((recentContacts / totalContacts) * 100) : 0;
document.getElementById('growth-rate').textContent = growthRate + '%';
}
renderCharts() {
// Activity distribution chart
const activityChart = document.getElementById('activity-chart');
const recentContacts = this.statsData.recent_contacts_24h || 0;
const recentAdverts = this.statsData.advertisements_24h || 0;
const total = recentContacts + recentAdverts;
if (total > 0) {
const contactsPercent = Math.round((recentContacts / total) * 100);
const advertsPercent = Math.round((recentAdverts / total) * 100);
const row = document.createElement('div');
row.className = 'row';
row.append(
this.makeMetricColumn(`${contactsPercent}%`, 'Contacts', 'text-primary'),
this.makeMetricColumn(`${advertsPercent}%`, 'Adverts', 'text-warning'),
);
activityChart.replaceChildren(row);
} else {
activityChart.replaceChildren(
this.makeTextElement('div', 'No recent activity', 'text-muted text-center'),
);
}
// Trends chart
const trendsChart = document.getElementById('trends-chart');
const growthRate = this.statsData.contact_count > 0 ?
Math.round(((this.statsData.recent_contacts_24h || 0) / this.statsData.contact_count) * 100) : 0;
const trend = document.createElement('div');
trend.className = 'text-center';
trend.append(
this.makeTextElement('h4', `${growthRate}%`, `text-${growthRate > 0 ? 'success' : 'secondary'}`),
this.makeTextElement('small', 'Daily growth rate', 'text-muted'),
);
trendsChart.replaceChildren(trend);
}
renderDetailedStats() {
// Contact statistics
const contactStats = document.getElementById('contact-stats');
this.renderLabeledList(contactStats, [
['Total Contacts', this.statsData.contact_count || 0],
['Recent (24h)', this.statsData.recent_contacts_24h || 0],
['Growth Rate', `${this.calculateGrowthRate()}%`],
['Last Updated', new Date().toLocaleString()],
], false);
// Advert statistics
const advertStats = document.getElementById('advert-stats');
this.renderLabeledList(advertStats, [
['Total Adverts', this.statsData.total_advertisements || 0],
['Recent (24h)', this.statsData.advertisements_24h || 0],
['Active Users', this.statsData.active_users_24h || 0],
['Last Updated', new Date().toLocaleString()],
], false);
}
renderSystemInfo() {
// System performance
const systemPerformance = document.getElementById('system-performance');
this.renderLabeledList(systemPerformance, [
['Connected Clients', this.statsData.connected_clients || 0],
['Database Status', 'Healthy'],
['Last Activity', new Date().toLocaleString()],
['Uptime', this.calculateUptime()],
]);
// System configuration
const systemConfig = document.getElementById('system-config');
this.renderLabeledList(systemConfig, [
['Version', 'Modern v2.0'],
['Flask-SocketIO', '5.x'],
['Database', 'SQLite'],
['Real-time', 'Enabled'],
]);
}
makeTextElement(tagName, value, className = '') {
const element = document.createElement(tagName);
element.className = className;
element.textContent = String(value ?? '');
return element;
}
makeMetricColumn(value, label, valueClass) {
const column = document.createElement('div');
column.className = 'col-6';
const content = document.createElement('div');
content.className = 'text-center';
content.append(
this.makeTextElement('h4', value, valueClass),
this.makeTextElement('small', label, 'text-muted'),
);
column.appendChild(content);
return column;
}
renderLabeledList(container, rows, wrap = true) {
const list = document.createElement(wrap ? 'ul' : 'div');
if (wrap) list.className = 'list-unstyled';
rows.forEach(([label, value]) => {
const item = document.createElement('li');
const strong = document.createElement('strong');
strong.textContent = `${label}:`;
item.append(strong, document.createTextNode(` ${String(value ?? '')}`));
list.appendChild(item);
});
container.replaceChildren(...(wrap ? [list] : Array.from(list.children)));
}
calculateGrowthRate() {
const recentContacts = this.statsData.recent_contacts_24h || 0;
const totalContacts = this.statsData.contact_count || 0;
return totalContacts > 0 ? Math.round((recentContacts / totalContacts) * 100) : 0;
}
calculateUptime() {
// This would be calculated based on actual system uptime
return 'Unknown';
}
showError(message) {
const errorDiv = document.createElement('div');
errorDiv.className = 'error';
errorDiv.textContent = message;
const content = document.querySelector('.container-fluid');
if (content) {
content.insertBefore(errorDiv, content.firstChild);
setTimeout(() => {
if (errorDiv.parentNode) {
errorDiv.parentNode.removeChild(errorDiv);
}
}, 5000);
}
}
}
// Initialize stats manager when page loads
document.addEventListener('DOMContentLoaded', () => {
window.statsManager = new ModernStatsManager();
});
</script>
{% endblock %}