Files
meshcore-bot/modules/web_viewer/templates/cache.html
agessaman 7bb51f219b Web Viewer Integration
- 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
2025-10-21 21:57:00 -07:00

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 %}