mirror of
https://github.com/agessaman/meshcore-bot.git
synced 2026-11-01 18:42:56 +00:00
Contacts and Cache inserted their alerts at the top of the content, so every save, star or error pushed the whole page down, and Radio, Feeds and the connection manager each pinned their own fixed alert at the same spot, so two messages covered each other. base.html now has one toast stack (window.viewerToast) that floats over the page below the navbar, or along the bottom on a phone, keeps at most four, announces them politely (errors as alerts) and takes either a string or a node the caller filled with text. Every page routes through it. The Contacts polish had turned the row actions into muted bare icons, which made them hard to find and to tell apart. Each action now has its own color and a light outline: a gold star (filled when starred), blue info, teal location and red delete, with icon contrast from 3.8:1 in light to 8.7:1 in dark. The new-device counts take their role colors, matching the chips and the activity chart.
319 lines
11 KiB
HTML
319 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 nonce="{{ g.csp_nonce }}">
|
|
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');
|
|
infoElement.replaceChildren();
|
|
if (!this.dbData.tables || this.dbData.tables.length === 0) {
|
|
const empty = document.createElement('div');
|
|
empty.className = 'text-muted';
|
|
empty.textContent = 'No tables found';
|
|
infoElement.appendChild(empty);
|
|
return;
|
|
}
|
|
|
|
const makeTextElement = (tag, value, classes = '') => {
|
|
const element = document.createElement(tag);
|
|
element.className = classes;
|
|
element.textContent = String(value ?? '');
|
|
return element;
|
|
};
|
|
this.dbData.tables.forEach(table => {
|
|
const row = document.createElement('div');
|
|
row.className = 'row mb-3 p-3 border rounded';
|
|
const identity = document.createElement('div');
|
|
identity.className = 'col-md-4';
|
|
identity.append(
|
|
makeTextElement('h6', table.name, 'mb-1'),
|
|
makeTextElement('small', table.description || 'Database table', 'text-muted'),
|
|
);
|
|
const metric = (value, label, width, valueClass = 'h6 mb-0') => {
|
|
const column = document.createElement('div');
|
|
column.className = width;
|
|
const content = document.createElement('div');
|
|
content.className = 'text-center';
|
|
content.append(
|
|
makeTextElement('div', value, valueClass),
|
|
makeTextElement('small', label, 'text-muted'),
|
|
);
|
|
column.appendChild(content);
|
|
return column;
|
|
};
|
|
const statusColumn = document.createElement('div');
|
|
statusColumn.className = 'col-md-3 text-center';
|
|
statusColumn.append(
|
|
makeTextElement(
|
|
'span',
|
|
this.getTableStatus(table),
|
|
`badge bg-${this.getTableStatusClass(table)}`,
|
|
),
|
|
document.createElement('br'),
|
|
makeTextElement('small', 'Status', 'text-muted'),
|
|
);
|
|
row.append(
|
|
identity,
|
|
metric(table.record_count || 0, 'Records', 'col-md-2', 'h5 mb-0 text-primary'),
|
|
metric(table.size || 'Unknown', 'Size', 'col-md-3'),
|
|
statusColumn,
|
|
);
|
|
infoElement.appendChild(row);
|
|
});
|
|
}
|
|
|
|
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',
|
|
'X-Requested-With': 'XMLHttpRequest'
|
|
}
|
|
});
|
|
|
|
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) {
|
|
window.viewerToast(String(message ?? ''), 'danger');
|
|
}
|
|
}
|
|
|
|
// Initialize database manager when page loads
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
window.databaseManager = new DatabaseInfoManager();
|
|
});
|
|
</script>
|
|
{% endblock %}
|