Add GPS-only backup/restore and database wipe
- Fix backup.py PRESERVED_FIELDS column names (lat/lng -> latitude/longitude) to match actual DB schema (pre-existing bug) - Add backup_gps() and restore_gps() dedicated GPS-only functions - Add /api/gps/backup and /api/gps/restore API endpoints - Add /api/gps/restore/upload for restoring from uploaded backup files - Add /api/db/wipe endpoint to clear all asset records - Update UI with GPS-only Backup button, Restore GPS from File picker, and Wipe All Assets with double-confirmation dialog - Fix backup listing to scan both seed-data/ and project root
This commit is contained in:
+81
-2
@@ -575,8 +575,9 @@
|
||||
<!-- Backup Management ───────────────────────────────────────────────── -->
|
||||
<div class="card" id="cardBackup" style="margin-top:20px">
|
||||
<h2>💾 Backup Management</h2>
|
||||
<div class="btn-row" style="margin-bottom:12px">
|
||||
<button class="btn btn-primary btn-sm" onclick="createBackup()">💾 Create Backup</button>
|
||||
<div class="btn-row" style="margin-bottom:8px">
|
||||
<button class="btn btn-primary btn-sm" onclick="createBackup()">💾 Full Backup</button>
|
||||
<button class="btn btn-sm" style="background:var(--accent2);color:#fff;border:none;cursor:pointer;border-radius:6px;padding:6px 14px;font-size:13px" onclick="createGpsBackup()">📍 GPS-Only Backup</button>
|
||||
<span id="backupStatus" style="font-size:13px;color:var(--text2)"></span>
|
||||
</div>
|
||||
<div id="backupList">
|
||||
@@ -584,6 +585,20 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Database Management ────────────────────────────────────────────── -->
|
||||
<div class="card" style="margin-top:20px;border-color:rgba(255,80,80,0.2);">
|
||||
<h2 style="color:var(--red);">⚠️ Database Management</h2>
|
||||
<div style="font-size:13px;color:var(--text3);margin-bottom:12px;">
|
||||
These operations modify the underlying asset database directly.
|
||||
Always <strong>backup GPS first</strong> to avoid losing coordinate data.
|
||||
</div>
|
||||
<div class="btn-row" style="gap:8px;flex-wrap:wrap;">
|
||||
<button class="btn btn-danger btn-sm" onclick="restoreFromFile()">📍 Restore GPS from File</button>
|
||||
<button class="btn btn-danger btn-sm" onclick="wipeDatabase()">🗑️ Wipe All Assets</button>
|
||||
<span id="dbManageStatus" style="font-size:13px;color:var(--text2)"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
@@ -999,6 +1014,70 @@
|
||||
setTimeout(loadBackupList, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── GPS-only Backup ──────────────────────────────────────────────
|
||||
|
||||
async function createGpsBackup() {
|
||||
const statusEl = document.getElementById('backupStatus');
|
||||
statusEl.innerHTML = '<span class="spinner"></span> Backing up GPS...';
|
||||
try {
|
||||
const res = await fetch('/api/gps/backup', { method: 'POST' });
|
||||
if (!res.ok) throw new Error((await res.json()).detail || 'GPS backup failed');
|
||||
const d = await res.json();
|
||||
statusEl.innerHTML = `<span style="color:var(--green)">✅ GPS backup: ${esc(d.backup_path)}</span>`;
|
||||
loadBackupList();
|
||||
refreshDashboard();
|
||||
} catch (e) {
|
||||
statusEl.innerHTML = `<span style="color:var(--red)">❌ ${esc(e.message)}</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Restore GPS from File (file picker) ──────────────────────────
|
||||
|
||||
function restoreFromFile() {
|
||||
// Create hidden file input for .json.gz files
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = '.json.gz,.json';
|
||||
input.onchange = async (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
const statusEl = document.getElementById('dbManageStatus');
|
||||
statusEl.innerHTML = `<span class="spinner"></span> Uploading & restoring GPS from ${esc(file.name)}...`;
|
||||
|
||||
// Upload to temp location then restore
|
||||
const form = new FormData();
|
||||
form.append('backup_file', file);
|
||||
try {
|
||||
const res = await fetch('/api/gps/restore/upload', { method: 'POST', body: form });
|
||||
if (!res.ok) throw new Error((await res.json()).detail || 'Restore failed');
|
||||
const d = await res.json();
|
||||
statusEl.innerHTML = `<span style="color:var(--green)">✅ GPS restored from: ${esc(d.restored_from)}</span>`;
|
||||
refreshDashboard();
|
||||
} catch (e) {
|
||||
statusEl.innerHTML = `<span style="color:var(--red)">❌ ${esc(e.message)}</span>`;
|
||||
}
|
||||
};
|
||||
input.click();
|
||||
}
|
||||
|
||||
// ─── Wipe Database ────────────────────────────────────────────────
|
||||
|
||||
async function wipeDatabase() {
|
||||
if (!confirm('⚠️ This will DELETE ALL ASSET RECORDS from the database. This cannot be undone without a backup.\n\nHave you backed up GPS coordinates first?')) return;
|
||||
if (!confirm('Final confirmation: Are you sure you want to delete ALL assets?')) return;
|
||||
const statusEl = document.getElementById('dbManageStatus');
|
||||
statusEl.innerHTML = '<span class="spinner"></span> Wiping database...';
|
||||
try {
|
||||
const res = await fetch('/api/db/wipe', { method: 'POST' });
|
||||
if (!res.ok) throw new Error((await res.json()).detail || 'Wipe failed');
|
||||
const d = await res.json();
|
||||
statusEl.innerHTML = `<span style="color:var(--red)">🗑️ Deleted ${d.records_deleted} records. Database is now empty.</span>`;
|
||||
refreshDashboard();
|
||||
} catch (e) {
|
||||
statusEl.innerHTML = `<span style="color:var(--red)">❌ ${esc(e.message)}</span>`;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user