mirror of
https://github.com/agessaman/meshcore-bot.git
synced 2026-08-05 10:19:51 +00:00
356 lines
12 KiB
HTML
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 %}
|