Add database maintenance UI with VACUUM and incremental vacuum
Settings > Advanced now shows database size, free pages, and auto- vacuum mode. Two actions: Compact Database (full VACUUM to reclaim dead space) and Enable Incremental Vacuum (one-time setup for automatic page reclamation). Both have confirmation dialogs warning about lock time on large databases. Info refreshes on Advanced tab switch and after each operation.
This commit is contained in:
parent
88890f816f
commit
23b80a0077
3 changed files with 221 additions and 0 deletions
110
web_server.py
110
web_server.py
|
|
@ -24126,6 +24126,116 @@ def download_backup_endpoint(filename):
|
|||
except Exception as e:
|
||||
return jsonify({"success": False, "error": str(e)}), 500
|
||||
|
||||
# ===============================
|
||||
# == DATABASE MAINTENANCE ==
|
||||
# ===============================
|
||||
|
||||
@app.route('/api/database/maintenance/info', methods=['GET'])
|
||||
def database_maintenance_info():
|
||||
"""Get database size, free pages, and auto_vacuum mode."""
|
||||
try:
|
||||
import sqlite3
|
||||
db_path = os.environ.get('DATABASE_PATH', 'database/music_library.db')
|
||||
conn = sqlite3.connect(db_path)
|
||||
c = conn.cursor()
|
||||
c.execute('PRAGMA page_count'); total_pages = c.fetchone()[0]
|
||||
c.execute('PRAGMA freelist_count'); free_pages = c.fetchone()[0]
|
||||
c.execute('PRAGMA page_size'); page_size = c.fetchone()[0]
|
||||
c.execute('PRAGMA auto_vacuum'); auto_vacuum = c.fetchone()[0]
|
||||
conn.close()
|
||||
|
||||
total_bytes = total_pages * page_size
|
||||
free_bytes = free_pages * page_size
|
||||
auto_vacuum_labels = {0: 'None', 1: 'Full', 2: 'Incremental'}
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'total_size': total_bytes,
|
||||
'total_size_display': f'{total_bytes / 1024 / 1024:.1f} MB',
|
||||
'free_pages': free_pages,
|
||||
'free_size': free_bytes,
|
||||
'free_size_display': f'{free_bytes / 1024 / 1024:.1f} MB',
|
||||
'bloat_percent': round(free_pages / total_pages * 100, 1) if total_pages > 0 else 0,
|
||||
'auto_vacuum': auto_vacuum,
|
||||
'auto_vacuum_label': auto_vacuum_labels.get(auto_vacuum, 'Unknown'),
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/database/maintenance/vacuum', methods=['POST'])
|
||||
def database_vacuum():
|
||||
"""Run VACUUM to compact the database. Locks DB during operation."""
|
||||
try:
|
||||
import sqlite3, time
|
||||
db_path = os.environ.get('DATABASE_PATH', 'database/music_library.db')
|
||||
|
||||
# Get size before
|
||||
size_before = os.path.getsize(db_path)
|
||||
|
||||
conn = sqlite3.connect(db_path)
|
||||
start = time.time()
|
||||
conn.execute('VACUUM')
|
||||
elapsed = time.time() - start
|
||||
conn.close()
|
||||
|
||||
size_after = os.path.getsize(db_path)
|
||||
saved = size_before - size_after
|
||||
|
||||
logger.info(f"Database VACUUM completed in {elapsed:.1f}s — saved {saved / 1024 / 1024:.1f} MB")
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'elapsed_seconds': round(elapsed, 1),
|
||||
'size_before': size_before,
|
||||
'size_after': size_after,
|
||||
'saved_bytes': saved,
|
||||
'saved_display': f'{saved / 1024 / 1024:.1f} MB',
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Database VACUUM failed: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/database/maintenance/enable-incremental-vacuum', methods=['POST'])
|
||||
def enable_incremental_vacuum():
|
||||
"""Enable incremental auto_vacuum. Requires a full VACUUM to activate."""
|
||||
try:
|
||||
import sqlite3, time
|
||||
db_path = os.environ.get('DATABASE_PATH', 'database/music_library.db')
|
||||
|
||||
conn = sqlite3.connect(db_path)
|
||||
c = conn.cursor()
|
||||
c.execute('PRAGMA auto_vacuum')
|
||||
current = c.fetchone()[0]
|
||||
|
||||
if current == 2:
|
||||
conn.close()
|
||||
return jsonify({'success': True, 'message': 'Incremental vacuum is already enabled', 'already_enabled': True})
|
||||
|
||||
size_before = os.path.getsize(db_path)
|
||||
|
||||
# Set incremental mode and VACUUM to activate it
|
||||
c.execute('PRAGMA auto_vacuum = INCREMENTAL')
|
||||
start = time.time()
|
||||
conn.execute('VACUUM')
|
||||
elapsed = time.time() - start
|
||||
conn.close()
|
||||
|
||||
size_after = os.path.getsize(db_path)
|
||||
saved = size_before - size_after
|
||||
|
||||
logger.info(f"Incremental auto_vacuum enabled in {elapsed:.1f}s — saved {saved / 1024 / 1024:.1f} MB")
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': 'Incremental vacuum enabled',
|
||||
'elapsed_seconds': round(elapsed, 1),
|
||||
'saved_display': f'{saved / 1024 / 1024:.1f} MB',
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to enable incremental vacuum: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
# ===============================
|
||||
# == METADATA CACHE API ==
|
||||
# ===============================
|
||||
|
|
|
|||
|
|
@ -5566,6 +5566,34 @@
|
|||
faster updates but more server load.</div>
|
||||
</div>
|
||||
|
||||
<!-- Database Maintenance -->
|
||||
<div class="settings-group" data-stg="advanced">
|
||||
<h3>Database Maintenance</h3>
|
||||
|
||||
<div class="form-group" id="db-maintenance-info">
|
||||
<label>Database Size:</label>
|
||||
<span id="db-size-display" class="readonly-field">Loading...</span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Free Pages:</label>
|
||||
<span id="db-freepages-display" class="readonly-field">Loading...</span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Auto-Vacuum Mode:</label>
|
||||
<span id="db-autovacuum-display" class="readonly-field">Loading...</span>
|
||||
</div>
|
||||
|
||||
<div class="form-actions" style="flex-wrap: wrap;">
|
||||
<button class="test-button" id="db-vacuum-btn" onclick="runDatabaseVacuum()">Compact Database (VACUUM)</button>
|
||||
<button class="test-button" id="db-incvacuum-btn" onclick="enableIncrementalVacuum()">Enable Incremental Vacuum</button>
|
||||
</div>
|
||||
<div class="help-text">
|
||||
<strong>Compact Database</strong> rewrites the entire DB to reclaim unused space. Locks the database briefly — may take a minute on large databases.<br>
|
||||
<strong>Incremental Vacuum</strong> enables automatic page reclamation. Requires a one-time full compact to activate. After that, freed pages are reclaimed in small batches automatically.
|
||||
</div>
|
||||
<div id="db-vacuum-status" style="display: none; margin-top: 8px; padding: 8px 12px; border-radius: 8px; font-size: 0.85em;"></div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Content Filter Settings -->
|
||||
<div class="settings-group" data-stg="library">
|
||||
|
|
|
|||
|
|
@ -5549,6 +5549,10 @@ function switchSettingsTab(tab) {
|
|||
if (typeof updateDownloadSourceUI === 'function') {
|
||||
try { updateDownloadSourceUI(); } catch(e) {}
|
||||
}
|
||||
// Load DB maintenance info when switching to Advanced tab
|
||||
if (tab === 'advanced' && typeof loadDbMaintenanceInfo === 'function') {
|
||||
try { loadDbMaintenanceInfo(); } catch(e) {}
|
||||
}
|
||||
}
|
||||
|
||||
function toggleStgService(el) {
|
||||
|
|
@ -6589,6 +6593,85 @@ async function toggleHydrabaseFromSettings() {
|
|||
}
|
||||
}
|
||||
|
||||
// ── Database Maintenance ──
|
||||
async function loadDbMaintenanceInfo() {
|
||||
try {
|
||||
const resp = await fetch('/api/database/maintenance/info');
|
||||
const data = await resp.json();
|
||||
if (!data.success) return;
|
||||
const sizeEl = document.getElementById('db-size-display');
|
||||
const freeEl = document.getElementById('db-freepages-display');
|
||||
const vacEl = document.getElementById('db-autovacuum-display');
|
||||
if (sizeEl) sizeEl.textContent = data.total_size_display;
|
||||
if (freeEl) freeEl.textContent = data.free_pages > 0
|
||||
? `${data.free_pages.toLocaleString()} (${data.free_size_display} reclaimable)`
|
||||
: 'None — database is fully compacted';
|
||||
if (vacEl) vacEl.textContent = data.auto_vacuum_label;
|
||||
// Hide enable button if already incremental
|
||||
const incBtn = document.getElementById('db-incvacuum-btn');
|
||||
if (incBtn && data.auto_vacuum === 2) {
|
||||
incBtn.textContent = 'Incremental Vacuum Enabled';
|
||||
incBtn.disabled = true;
|
||||
incBtn.style.opacity = '0.5';
|
||||
}
|
||||
} catch (e) { console.error('Error loading DB maintenance info:', e); }
|
||||
}
|
||||
|
||||
async function runDatabaseVacuum() {
|
||||
const btn = document.getElementById('db-vacuum-btn');
|
||||
const status = document.getElementById('db-vacuum-status');
|
||||
if (!confirm('This will compact the database by rewriting it. The database will be locked during this operation. For large databases this may take over a minute. Continue?')) return;
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Compacting...';
|
||||
if (status) { status.style.display = 'block'; status.style.background = 'rgba(255,255,255,0.04)'; status.style.color = 'rgba(255,255,255,0.6)'; status.textContent = 'Running VACUUM — this may take a while...'; }
|
||||
try {
|
||||
const resp = await fetch('/api/database/maintenance/vacuum', { method: 'POST' });
|
||||
const data = await resp.json();
|
||||
if (data.success) {
|
||||
showToast(`Database compacted in ${data.elapsed_seconds}s — saved ${data.saved_display}`, 'success');
|
||||
if (status) { status.style.color = '#4caf50'; status.textContent = `Done in ${data.elapsed_seconds}s. Saved ${data.saved_display}.`; }
|
||||
loadDbMaintenanceInfo();
|
||||
} else {
|
||||
showToast('Vacuum failed: ' + (data.error || 'Unknown error'), 'error');
|
||||
if (status) { status.style.color = '#ef5350'; status.textContent = 'Failed: ' + (data.error || 'Unknown error'); }
|
||||
}
|
||||
} catch (e) {
|
||||
showToast('Vacuum failed: ' + e.message, 'error');
|
||||
if (status) { status.style.color = '#ef5350'; status.textContent = 'Failed: ' + e.message; }
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Compact Database (VACUUM)';
|
||||
}
|
||||
}
|
||||
|
||||
async function enableIncrementalVacuum() {
|
||||
const btn = document.getElementById('db-incvacuum-btn');
|
||||
const status = document.getElementById('db-vacuum-status');
|
||||
if (!confirm('This will enable incremental vacuum mode. It requires a one-time full VACUUM to activate, which locks the database and may take over a minute on large databases. Continue?')) return;
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Enabling...';
|
||||
if (status) { status.style.display = 'block'; status.style.background = 'rgba(255,255,255,0.04)'; status.style.color = 'rgba(255,255,255,0.6)'; status.textContent = 'Enabling incremental vacuum — this may take a while...'; }
|
||||
try {
|
||||
const resp = await fetch('/api/database/maintenance/enable-incremental-vacuum', { method: 'POST' });
|
||||
const data = await resp.json();
|
||||
if (data.success) {
|
||||
const msg = data.already_enabled ? 'Already enabled' : `Enabled in ${data.elapsed_seconds}s — saved ${data.saved_display}`;
|
||||
showToast(msg, 'success');
|
||||
if (status) { status.style.color = '#4caf50'; status.textContent = msg; }
|
||||
loadDbMaintenanceInfo();
|
||||
} else {
|
||||
showToast('Failed: ' + (data.error || 'Unknown error'), 'error');
|
||||
if (status) { status.style.color = '#ef5350'; status.textContent = 'Failed: ' + (data.error || 'Unknown error'); }
|
||||
}
|
||||
} catch (e) {
|
||||
showToast('Failed: ' + e.message, 'error');
|
||||
if (status) { status.style.color = '#ef5350'; status.textContent = 'Failed: ' + e.message; }
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Enable Incremental Vacuum';
|
||||
}
|
||||
}
|
||||
|
||||
async function activateDevMode() {
|
||||
const password = document.getElementById('dev-mode-password').value;
|
||||
try {
|
||||
|
|
|
|||
Loading…
Reference in a new issue