db management system added
This commit is contained in:
@@ -401,6 +401,99 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DATABASE MANAGEMENT SECTION -->
|
||||
<div class="mt-8 mb-4">
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-white">Database Management</h2>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">Create snapshots, restore backups, and manage database files</p>
|
||||
</div>
|
||||
|
||||
<!-- Database Statistics -->
|
||||
<div class="border-2 border-blue-300 dark:border-blue-800 rounded-lg p-6 bg-white dark:bg-slate-800">
|
||||
<h3 class="font-semibold text-blue-600 dark:text-blue-400 text-lg mb-3">Database Statistics</h3>
|
||||
<div id="dbStatsLoading" class="text-center py-4">
|
||||
<div class="inline-block animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600"></div>
|
||||
</div>
|
||||
<div id="dbStatsContent" class="hidden">
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
||||
<div>
|
||||
<p class="text-gray-500 dark:text-gray-400">Database Size</p>
|
||||
<p id="dbSize" class="text-lg font-semibold text-gray-900 dark:text-white">-</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-500 dark:text-gray-400">Total Rows</p>
|
||||
<p id="dbRows" class="text-lg font-semibold text-gray-900 dark:text-white">-</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-500 dark:text-gray-400">Last Modified</p>
|
||||
<p id="dbModified" class="text-lg font-semibold text-gray-900 dark:text-white">-</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-500 dark:text-gray-400">Snapshots</p>
|
||||
<p id="dbSnapshotCount" class="text-lg font-semibold text-gray-900 dark:text-white">-</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button onclick="loadDatabaseStats()" class="mt-4 px-4 py-2 text-blue-600 hover:bg-blue-50 dark:hover:bg-blue-900/20 rounded-lg transition-colors text-sm">
|
||||
Refresh Stats
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Create Snapshot -->
|
||||
<div class="border border-green-200 dark:border-green-800 rounded-lg p-6 bg-white dark:bg-slate-800">
|
||||
<div class="flex justify-between items-start">
|
||||
<div class="flex-1">
|
||||
<h3 class="font-semibold text-green-600 dark:text-green-400">Create Database Snapshot</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
||||
Create a full backup of the current database state
|
||||
</p>
|
||||
</div>
|
||||
<button onclick="createSnapshot()" class="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-lg transition-colors whitespace-nowrap">
|
||||
Create Snapshot
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Snapshots List -->
|
||||
<div class="border border-gray-200 dark:border-gray-700 rounded-lg p-6 bg-white dark:bg-slate-800">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="font-semibold text-gray-900 dark:text-white">Available Snapshots</h3>
|
||||
<button onclick="loadSnapshots()" class="px-3 py-1 text-sm text-seismo-orange hover:bg-orange-50 dark:hover:bg-orange-900/20 rounded transition-colors">
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="snapshotsLoading" class="text-center py-4">
|
||||
<div class="inline-block animate-spin rounded-full h-6 w-6 border-b-2 border-seismo-orange"></div>
|
||||
</div>
|
||||
|
||||
<div id="snapshotsList" class="hidden space-y-2">
|
||||
<!-- Snapshots will be inserted here -->
|
||||
</div>
|
||||
|
||||
<div id="snapshotsEmpty" class="hidden text-center py-8 text-gray-500 dark:text-gray-400">
|
||||
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4"></path>
|
||||
</svg>
|
||||
<p class="mt-2">No snapshots found</p>
|
||||
<p class="text-sm">Create your first snapshot above</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Upload Snapshot -->
|
||||
<div class="border border-purple-200 dark:border-purple-800 rounded-lg p-6 bg-white dark:bg-slate-800">
|
||||
<h3 class="font-semibold text-purple-600 dark:text-purple-400 mb-2">Upload Snapshot</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
||||
Upload a database snapshot file from another server
|
||||
</p>
|
||||
<form id="uploadSnapshotForm" class="space-y-3">
|
||||
<input type="file" accept=".db" id="snapshotFileInput" class="block w-full text-sm text-gray-900 dark:text-white border border-gray-300 dark:border-gray-600 rounded-lg cursor-pointer bg-gray-50 dark:bg-slate-700">
|
||||
<button type="submit" class="px-4 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-lg transition-colors">
|
||||
Upload Snapshot
|
||||
</button>
|
||||
</form>
|
||||
<div id="uploadResult" class="hidden mt-3"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1004,5 +1097,263 @@ async function confirmClearIgnored() {
|
||||
alert('❌ Error: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// ========== DATABASE MANAGEMENT ==========
|
||||
|
||||
async function loadDatabaseStats() {
|
||||
const loading = document.getElementById('dbStatsLoading');
|
||||
const content = document.getElementById('dbStatsContent');
|
||||
|
||||
try {
|
||||
loading.classList.remove('hidden');
|
||||
content.classList.add('hidden');
|
||||
|
||||
const response = await fetch('/api/settings/database/stats');
|
||||
const stats = await response.json();
|
||||
|
||||
// Update stats display
|
||||
document.getElementById('dbSize').textContent = stats.size_mb + ' MB';
|
||||
document.getElementById('dbRows').textContent = stats.total_rows.toLocaleString();
|
||||
|
||||
const lastMod = new Date(stats.last_modified);
|
||||
document.getElementById('dbModified').textContent = lastMod.toLocaleDateString();
|
||||
|
||||
// Load snapshot count
|
||||
const snapshotsResp = await fetch('/api/settings/database/snapshots');
|
||||
const snapshotsData = await snapshotsResp.json();
|
||||
document.getElementById('dbSnapshotCount').textContent = snapshotsData.count;
|
||||
|
||||
loading.classList.add('hidden');
|
||||
content.classList.remove('hidden');
|
||||
} catch (error) {
|
||||
loading.classList.add('hidden');
|
||||
alert('Error loading database stats: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function createSnapshot() {
|
||||
const description = prompt('Enter a description for this snapshot (optional):');
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/settings/database/snapshot', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ description: description || null })
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
alert(`✅ Snapshot created successfully!\n\nFilename: ${result.snapshot.filename}\nSize: ${result.snapshot.size_mb} MB`);
|
||||
loadSnapshots();
|
||||
loadDatabaseStats();
|
||||
} else {
|
||||
alert('❌ Error: ' + (result.detail || 'Unknown error'));
|
||||
}
|
||||
} catch (error) {
|
||||
alert('❌ Error: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSnapshots() {
|
||||
const loading = document.getElementById('snapshotsLoading');
|
||||
const list = document.getElementById('snapshotsList');
|
||||
const empty = document.getElementById('snapshotsEmpty');
|
||||
|
||||
try {
|
||||
loading.classList.remove('hidden');
|
||||
list.classList.add('hidden');
|
||||
empty.classList.add('hidden');
|
||||
|
||||
const response = await fetch('/api/settings/database/snapshots');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.snapshots.length === 0) {
|
||||
loading.classList.add('hidden');
|
||||
empty.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
list.innerHTML = data.snapshots.map(snapshot => createSnapshotCard(snapshot)).join('');
|
||||
|
||||
loading.classList.add('hidden');
|
||||
list.classList.remove('hidden');
|
||||
} catch (error) {
|
||||
loading.classList.add('hidden');
|
||||
alert('Error loading snapshots: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
function createSnapshotCard(snapshot) {
|
||||
const createdDate = new Date(snapshot.created_at_iso);
|
||||
const dateStr = createdDate.toLocaleString();
|
||||
|
||||
return `
|
||||
<div class="border border-gray-200 dark:border-gray-700 rounded-lg p-4 bg-gray-50 dark:bg-gray-700/50">
|
||||
<div class="flex justify-between items-start">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<h4 class="font-medium text-gray-900 dark:text-white">${snapshot.filename}</h4>
|
||||
<span class="text-xs px-2 py-1 rounded ${snapshot.type === 'manual' ? 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300' : 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-300'}">
|
||||
${snapshot.type}
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">${snapshot.description}</p>
|
||||
<div class="flex gap-4 mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
<span>📅 ${dateStr}</span>
|
||||
<span>💾 ${snapshot.size_mb} MB</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2 ml-4">
|
||||
<button onclick="downloadSnapshot('${snapshot.filename}')"
|
||||
class="p-2 hover:bg-gray-200 dark:hover:bg-gray-600 rounded transition-colors text-blue-600 dark:text-blue-400"
|
||||
title="Download">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button onclick="restoreSnapshot('${snapshot.filename}')"
|
||||
class="p-2 hover:bg-gray-200 dark:hover:bg-gray-600 rounded transition-colors text-green-600 dark:text-green-400"
|
||||
title="Restore">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button onclick="deleteSnapshot('${snapshot.filename}')"
|
||||
class="p-2 hover:bg-gray-200 dark:hover:bg-gray-600 rounded transition-colors text-red-600 dark:text-red-400"
|
||||
title="Delete">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function downloadSnapshot(filename) {
|
||||
window.location.href = `/api/settings/database/snapshot/${filename}`;
|
||||
}
|
||||
|
||||
async function restoreSnapshot(filename) {
|
||||
const confirmMsg = `⚠️ RESTORE DATABASE WARNING ⚠️
|
||||
|
||||
This will REPLACE the current database with snapshot:
|
||||
${filename}
|
||||
|
||||
A backup of the current database will be created automatically before restoring.
|
||||
|
||||
THIS ACTION WILL RESTART THE APPLICATION!
|
||||
|
||||
Continue?`;
|
||||
|
||||
if (!confirm(confirmMsg)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/settings/database/restore', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
filename: filename,
|
||||
create_backup: true
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
alert(`✅ Database restored successfully!\n\nRestored from: ${result.restored_from}\nBackup created: ${result.backup_created}\n\nThe page will now reload.`);
|
||||
location.reload();
|
||||
} else {
|
||||
alert('❌ Error: ' + (result.detail || 'Unknown error'));
|
||||
}
|
||||
} catch (error) {
|
||||
alert('❌ Error: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteSnapshot(filename) {
|
||||
if (!confirm(`Delete snapshot ${filename}?\n\nThis cannot be undone.`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/settings/database/snapshot/${filename}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
alert(`✅ Snapshot deleted: ${filename}`);
|
||||
loadSnapshots();
|
||||
loadDatabaseStats();
|
||||
} else {
|
||||
alert('❌ Error: ' + (result.detail || 'Unknown error'));
|
||||
}
|
||||
} catch (error) {
|
||||
alert('❌ Error: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Upload snapshot form handler
|
||||
document.getElementById('uploadSnapshotForm').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const fileInput = document.getElementById('snapshotFileInput');
|
||||
const resultDiv = document.getElementById('uploadResult');
|
||||
|
||||
if (!fileInput.files[0]) {
|
||||
alert('Please select a file');
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', fileInput.files[0]);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/settings/database/upload-snapshot', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
resultDiv.className = 'mt-3 p-3 rounded-lg bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200';
|
||||
resultDiv.innerHTML = `✅ Uploaded: ${result.snapshot.filename} (${result.snapshot.size_mb} MB)`;
|
||||
resultDiv.classList.remove('hidden');
|
||||
|
||||
fileInput.value = '';
|
||||
loadSnapshots();
|
||||
loadDatabaseStats();
|
||||
|
||||
setTimeout(() => {
|
||||
resultDiv.classList.add('hidden');
|
||||
}, 5000);
|
||||
} else {
|
||||
resultDiv.className = 'mt-3 p-3 rounded-lg bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200';
|
||||
resultDiv.innerHTML = `❌ Error: ${result.detail || 'Unknown error'}`;
|
||||
resultDiv.classList.remove('hidden');
|
||||
}
|
||||
} catch (error) {
|
||||
resultDiv.className = 'mt-3 p-3 rounded-lg bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200';
|
||||
resultDiv.innerHTML = `❌ Error: ${error.message}`;
|
||||
resultDiv.classList.remove('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
// Load database stats and snapshots when danger zone tab is shown
|
||||
const originalShowTab = showTab;
|
||||
showTab = function(tabName) {
|
||||
originalShowTab(tabName);
|
||||
if (tabName === 'danger') {
|
||||
loadDatabaseStats();
|
||||
loadSnapshots();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user