mirror of
https://github.com/agessaman/meshcore-bot.git
synced 2026-03-30 20:15:40 +00:00
- Add Flask + Flask-SocketIO web viewer with dashboard (modules/web_viewer/app.py and related) - Add web viewer templates: index, realtime, tracking (contacts), cache, purging, stats (modules/web_viewer/templates/) - Add integration hooks and utility functions for web viewer (modules/web_viewer/integration.py, modules/utils.py) - Add command to launch web viewer from bot CLI (modules/commands/webviewer_command.py) - Update .gitignore: ignore db/log files, test scripts, and web viewer artifacts - Add restart_viewer.sh helper script for standalone web viewer restart/troubleshooting - Add guidance and documentation for modern viewer in WEB_VIEWER.md and docs/ - Various code structure and import improvements to core bot and command modules to support integration - Add ACL support for sensitive commands - Example config updates Benefits: - Decouples monitoring/UI from bot core process - Enables real-time browser dashboard and unified contact/repeater tracking - Easier integration, dev, and troubleshooting
313 lines
11 KiB
HTML
313 lines
11 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Database Information - MeshCore Bot Data Viewer{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<h1 class="mb-4">
|
|
<i class="fas fa-database"></i> Database Information
|
|
</h1>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Database Overview -->
|
|
<div class="row mb-4">
|
|
<div class="col-md-3">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<i class="fas fa-table"></i> Total Tables
|
|
</div>
|
|
<div class="card-body">
|
|
<h3 id="total-tables">0</h3>
|
|
<small class="text-muted">Database tables</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-3">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<i class="fas fa-list"></i> Total Records
|
|
</div>
|
|
<div class="card-body">
|
|
<h3 id="total-records">0</h3>
|
|
<small class="text-muted">All records</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-3">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<i class="fas fa-clock"></i> Last Updated
|
|
</div>
|
|
<div class="card-body">
|
|
<h6 id="last-updated">-</h6>
|
|
<small class="text-muted">Database activity</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-3">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<i class="fas fa-hdd"></i> Database Size
|
|
</div>
|
|
<div class="card-body">
|
|
<h6 id="db-size">-</h6>
|
|
<small class="text-muted">Storage used</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Database Controls -->
|
|
<div class="row mb-4">
|
|
<div class="col-12">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<i class="fas fa-cog"></i> Database Operations
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="btn-group" role="group">
|
|
<button class="btn btn-primary" id="refresh-db">
|
|
<i class="fas fa-sync"></i> Refresh
|
|
</button>
|
|
<button class="btn btn-info" id="export-db">
|
|
<i class="fas fa-download"></i> Export
|
|
</button>
|
|
<button class="btn btn-warning" id="optimize-db">
|
|
<i class="fas fa-tools"></i> Optimize
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Database Tables -->
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<i class="fas fa-list"></i> Database Tables
|
|
</div>
|
|
<div class="card-body">
|
|
<div id="tables-info">
|
|
<div class="loading">Loading database information...</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
<script>
|
|
class DatabaseInfoManager {
|
|
constructor() {
|
|
this.dbData = {};
|
|
this.initializeDatabase();
|
|
}
|
|
|
|
async initializeDatabase() {
|
|
await this.loadDatabaseData();
|
|
this.setupEventHandlers();
|
|
this.updateStatistics();
|
|
this.renderTablesInfo();
|
|
}
|
|
|
|
async loadDatabaseData() {
|
|
try {
|
|
const response = await fetch('/api/database');
|
|
const data = await response.json();
|
|
|
|
if (data.error) {
|
|
this.showError('Failed to load database data: ' + data.error);
|
|
return;
|
|
}
|
|
|
|
this.dbData = data;
|
|
|
|
} catch (error) {
|
|
console.error('Error loading database data:', error);
|
|
this.showError('Failed to load database data: ' + error.message);
|
|
}
|
|
}
|
|
|
|
setupEventHandlers() {
|
|
document.getElementById('refresh-db').addEventListener('click', () => {
|
|
this.loadDatabaseData().then(() => {
|
|
this.updateStatistics();
|
|
this.renderTablesInfo();
|
|
});
|
|
});
|
|
|
|
document.getElementById('export-db').addEventListener('click', () => {
|
|
this.exportDatabase();
|
|
});
|
|
|
|
document.getElementById('optimize-db').addEventListener('click', () => {
|
|
this.optimizeDatabase();
|
|
});
|
|
}
|
|
|
|
updateStatistics() {
|
|
document.getElementById('total-tables').textContent = this.dbData.total_tables || 0;
|
|
document.getElementById('total-records').textContent = this.dbData.total_records || 0;
|
|
document.getElementById('last-updated').textContent = this.dbData.last_updated || '-';
|
|
document.getElementById('db-size').textContent = this.dbData.db_size || '-';
|
|
}
|
|
|
|
renderTablesInfo() {
|
|
const infoElement = document.getElementById('tables-info');
|
|
|
|
if (!this.dbData.tables || this.dbData.tables.length === 0) {
|
|
infoElement.innerHTML = '<div class="text-muted">No tables found</div>';
|
|
return;
|
|
}
|
|
|
|
const tablesHtml = this.dbData.tables.map(table => `
|
|
<div class="row mb-3 p-3 border rounded">
|
|
<div class="col-md-4">
|
|
<h6 class="mb-1">
|
|
<i class="fas fa-table text-primary"></i> ${table.name}
|
|
</h6>
|
|
<small class="text-muted">${table.description || 'Database table'}</small>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<div class="text-center">
|
|
<h5 class="mb-0 text-primary">${table.record_count || 0}</h5>
|
|
<small class="text-muted">Records</small>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="text-center">
|
|
<h6 class="mb-0">${table.size || 'Unknown'}</h6>
|
|
<small class="text-muted">Size</small>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="text-center">
|
|
<span class="badge bg-${this.getTableStatusClass(table)}">${this.getTableStatus(table)}</span>
|
|
<br>
|
|
<small class="text-muted">Status</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
|
|
infoElement.innerHTML = tablesHtml;
|
|
}
|
|
|
|
getTableStatus(table) {
|
|
const count = table.record_count || 0;
|
|
if (count === 0) return 'Empty';
|
|
if (count < 100) return 'Small';
|
|
if (count < 1000) return 'Medium';
|
|
if (count < 10000) return 'Large';
|
|
return 'Very Large';
|
|
}
|
|
|
|
getTableStatusClass(table) {
|
|
const count = table.record_count || 0;
|
|
if (count === 0) return 'secondary';
|
|
if (count < 100) return 'success';
|
|
if (count < 1000) return 'info';
|
|
if (count < 10000) return 'warning';
|
|
return 'danger';
|
|
}
|
|
|
|
exportDatabase() {
|
|
const csvContent = this.generateDatabaseCSV();
|
|
const blob = new Blob([csvContent], { type: 'text/csv' });
|
|
const url = window.URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = `database_export_${new Date().toISOString().split('T')[0]}.csv`;
|
|
a.click();
|
|
window.URL.revokeObjectURL(url);
|
|
}
|
|
|
|
generateDatabaseCSV() {
|
|
const headers = ['Table Name', 'Record Count', 'Size', 'Status', 'Timestamp'];
|
|
const rows = (this.dbData.tables || []).map(table => [
|
|
table.name,
|
|
table.record_count || 0,
|
|
table.size || 'Unknown',
|
|
this.getTableStatus(table),
|
|
new Date().toISOString()
|
|
]);
|
|
|
|
return [headers, ...rows].map(row =>
|
|
row.map(field => `"${field}"`).join(',')
|
|
).join('\n');
|
|
}
|
|
|
|
async optimizeDatabase() {
|
|
if (confirm('Are you sure you want to optimize the database? This may take a few moments.')) {
|
|
try {
|
|
// Show loading state
|
|
const optimizeBtn = document.getElementById('optimize-db');
|
|
const originalText = optimizeBtn.innerHTML;
|
|
optimizeBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Optimizing...';
|
|
optimizeBtn.disabled = true;
|
|
|
|
// Call backend optimization
|
|
const response = await fetch('/api/optimize-database', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
}
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
alert(`Database optimization completed successfully!\n\nResults:\n- ${result.vacuum_result}\n- ${result.analyze_result}\n- ${result.reindex_result}`);
|
|
// Refresh the database information
|
|
await this.loadDatabaseData();
|
|
this.updateStatistics();
|
|
this.renderTablesInfo();
|
|
} else {
|
|
alert('Database optimization failed: ' + (result.error || 'Unknown error'));
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Error optimizing database:', error);
|
|
alert('Database optimization failed: ' + error.message);
|
|
} finally {
|
|
// Restore button state
|
|
const optimizeBtn = document.getElementById('optimize-db');
|
|
optimizeBtn.innerHTML = '<i class="fas fa-tools"></i> Optimize';
|
|
optimizeBtn.disabled = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
showError(message) {
|
|
const errorDiv = document.createElement('div');
|
|
errorDiv.className = 'alert alert-danger';
|
|
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 database manager when page loads
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
window.databaseManager = new DatabaseInfoManager();
|
|
});
|
|
</script>
|
|
{% endblock %}
|