Style backup actions and add delete control
This commit is contained in:
+2
-1
@@ -88,10 +88,11 @@ function restockForm(p){ openModal(`<h2>Void sale & restock</h2><p>Return ${esc(
|
||||
async function load(){ state.products=await api("/api/products"); render(); }
|
||||
async function renderAdmin(){
|
||||
const panel=$("#adminPanel"); panel.innerHTML='<div class="admin-loading">Loading backups…</div>';
|
||||
try{const backups=await api("/api/admin/backups");panel.innerHTML=`<div class="admin-page"><div><h2>Database backups</h2><p>Create snapshots inside the persistent <code>/data/backups</code> folder, download an off-server copy, or restore a previous database.</p></div><div class="admin-actions"><button class="primary" id="createBackup">Create backup now</button><label class="upload-backup">Restore uploaded backup<input id="restoreUpload" type="file" accept=".db,application/vnd.sqlite3"></label></div><div class="backup-warning"><strong>Restore replaces the active database.</strong> The app automatically creates a pre-restore backup and restarts the container.</div><div class="backup-list">${backups.length?backups.map(b=>`<article><div><strong>${esc(b.name)}</strong><small>${fmtDateTime(b.createdAt)} · ${(b.size/1024).toFixed(1)} KB</small></div><div><a class="button secondary" href="/api/admin/backups/${encodeURIComponent(b.name)}/download">Download</a><button class="secondary" data-restore-backup="${esc(b.name)}">Restore</button></div></article>`).join(""):"<p>No local backups yet.</p>"}</div></div>`;
|
||||
try{const backups=await api("/api/admin/backups");panel.innerHTML=`<div class="admin-page"><div><h2>Database backups</h2><p>Create snapshots inside the persistent <code>/data/backups</code> folder, download an off-server copy, or restore a previous database.</p></div><div class="admin-actions"><button class="primary" id="createBackup">Create backup now</button><label class="upload-backup">Restore uploaded backup<input id="restoreUpload" type="file" accept=".db,application/vnd.sqlite3"></label></div><div class="backup-warning"><strong>Restore replaces the active database.</strong> The app automatically creates a pre-restore backup and restarts the container.</div><div class="backup-list">${backups.length?backups.map(b=>`<article><div><strong>${esc(b.name)}</strong><small>${fmtDateTime(b.createdAt)} · ${(b.size/1024).toFixed(1)} KB</small></div><div><a class="button secondary" href="/api/admin/backups/${encodeURIComponent(b.name)}/download">Download</a><button class="secondary" data-restore-backup="${esc(b.name)}">Restore</button><button class="delete-backup" data-delete-backup="${esc(b.name)}">Delete</button></div></article>`).join(""):"<p>No local backups yet.</p>"}</div></div>`;
|
||||
$("#createBackup").onclick=async()=>{try{await api("/api/admin/backups",{method:"POST",body:"{}"});toast("Database backup created.");renderAdmin();}catch(e){toast(e.message);}};
|
||||
$("#restoreUpload").onchange=async e=>{const file=e.target.files[0];if(!file||!confirm(`Restore ${file.name}? Current data will be replaced and the container will restart.`))return;await restoreUpload(file);};
|
||||
document.querySelectorAll("[data-restore-backup]").forEach(b=>b.onclick=async()=>{if(!confirm(`Restore ${b.dataset.restoreBackup}? Current data will be replaced and the container will restart.`))return;try{storageStatus("saving","Restoring database");await api(`/api/admin/backups/${encodeURIComponent(b.dataset.restoreBackup)}/restore`,{method:"POST",body:"{}"});panel.innerHTML='<div class="restart-message"><h2>Restore complete</h2><p>The container is restarting. Refresh this page in a few seconds.</p></div>';}catch(e){toast(e.message);}});
|
||||
document.querySelectorAll("[data-delete-backup]").forEach(b=>b.onclick=async()=>{if(!confirm(`Permanently delete backup ${b.dataset.deleteBackup}?`))return;try{await api(`/api/admin/backups/${encodeURIComponent(b.dataset.deleteBackup)}`,{method:"DELETE"});toast("Backup deleted.");renderAdmin();}catch(e){toast(e.message);}});
|
||||
}catch(e){panel.innerHTML=`<div class="restart-message"><h2>Unable to load backups</h2><p>${esc(e.message)}</p></div>`;}
|
||||
}
|
||||
async function restoreUpload(file){const panel=$("#adminPanel");try{storageStatus("saving","Restoring database");const response=await fetch("/api/admin/restore-upload",{method:"POST",headers:{"content-type":"application/octet-stream"},body:file});const data=await response.json();if(!response.ok)throw new Error(data.error||"Restore failed");panel.innerHTML='<div class="restart-message"><h2>Restore complete</h2><p>The container is restarting. Refresh this page in a few seconds.</p></div>';}catch(e){storageStatus("critical","Database error");toast(e.message);}}
|
||||
|
||||
+1
-1
@@ -3,4 +3,4 @@
|
||||
.address-grid{display:grid;grid-template-columns:2fr 1fr 1fr;gap:10px}.detail-card{background:#f7f9fc;border:1px solid #e2e7ef;border-radius:10px;padding:14px;margin:14px 0}.detail-card h3,.customer-purchases h3{margin:0 0 10px}.detail-card p{margin:4px 0!important}.customer-purchases{max-height:330px;overflow:auto}.customer-purchases article{border-top:1px solid #e5e9f0;padding:12px 0}.customer-purchases article:first-child{border-top:0}.customer-purchases button{padding:6px 9px;background:#edf3ff;color:#1d4ed8}@media(max-width:760px){.address-grid{grid-template-columns:1fr}#pagination{align-items:flex-start;flex-direction:column}#pagination>div{width:100%;align-items:center;justify-content:space-between}}
|
||||
.customer-notes{margin:18px 0}.customer-notes h3{margin-bottom:8px}.customer-notes article{position:relative;border:1px solid #e2e7ef;border-radius:9px;padding:12px;margin:8px 0}.customer-notes article>div{display:flex;align-items:center;gap:8px;color:#667085}.customer-notes article p{white-space:pre-wrap;margin:10px 0}.customer-notes article>button{position:absolute;right:8px;top:8px;padding:5px 8px;background:#fff0f0;color:#c62828}.note-category{background:#edf3ff;color:#1d4ed8;border-radius:99px;padding:3px 8px;font-size:12px;font-weight:700}
|
||||
.storage-status{margin-left:auto!important;display:flex;align-items:center;gap:7px;white-space:nowrap}.storage-status i{width:9px;height:9px;border-radius:50%;background:#f59e0b;box-shadow:0 0 0 3px #f59e0b22}.storage-status b{font-weight:600}.storage-status.connected i{background:#16a34a;box-shadow:0 0 0 3px #16a34a22}.storage-status.saving i,.storage-status.connecting i{background:#f59e0b;box-shadow:0 0 0 3px #f59e0b22;animation:status-pulse 1s infinite}.storage-status.critical{color:#b42318!important}.storage-status.critical i{background:#dc2626;box-shadow:0 0 0 3px #dc262622}@keyframes status-pulse{50%{opacity:.4}}@media(max-width:760px){.storage-status{display:flex!important;flex-basis:100%;margin-left:0!important;padding-bottom:10px}.records nav{height:auto;flex-wrap:wrap}}
|
||||
#adminPanel{padding:24px}.admin-page{display:grid;gap:20px}.admin-page h2{margin:0 0 5px}.admin-page p{color:#667085}.admin-actions{display:flex;gap:10px;flex-wrap:wrap}.upload-backup{display:inline-flex;margin:0;background:#fff;border:1px solid #d7deea;border-radius:9px;padding:10px 14px;color:#27344a;cursor:pointer}.upload-backup input{display:none}.backup-warning{padding:13px 15px;border-radius:9px;background:#fff7ed;color:#9a3412;border:1px solid #fed7aa}.backup-list{border:1px solid #e3e7ef;border-radius:10px}.backup-list article{display:flex;justify-content:space-between;align-items:center;gap:15px;padding:14px;border-bottom:1px solid #edf0f5}.backup-list article:last-child{border-bottom:0}.backup-list article small{display:block;color:#7b8799;margin-top:4px}.backup-list article>div:last-child{display:flex;gap:8px}.backup-list a{text-decoration:none}.restart-message{text-align:center;padding:60px 20px}@media(max-width:760px){.backup-list article{align-items:flex-start;flex-direction:column}}
|
||||
#adminPanel{padding:24px}.admin-page{display:grid;gap:20px}.admin-page h2{margin:0 0 5px}.admin-page p{color:#667085}.admin-actions{display:flex;gap:10px;flex-wrap:wrap}.upload-backup{display:inline-flex;margin:0;background:#fff;border:1px solid #d7deea;border-radius:9px;padding:10px 14px;color:#27344a;cursor:pointer}.upload-backup input{display:none}.backup-warning{padding:13px 15px;border-radius:9px;background:#fff7ed;color:#9a3412;border:1px solid #fed7aa}.backup-list{border:1px solid #e3e7ef;border-radius:10px}.backup-list article{display:flex;justify-content:space-between;align-items:center;gap:15px;padding:14px;border-bottom:1px solid #edf0f5}.backup-list article:last-child{border-bottom:0}.backup-list article small{display:block;color:#7b8799;margin-top:4px}.backup-list article>div:last-child{display:flex;gap:8px}.backup-list a.button{display:inline-flex;align-items:center;background:#fff;border:1px solid #d7deea;border-radius:9px;padding:10px 14px;color:#27344a;font-weight:650;text-decoration:none}.backup-list .delete-backup{background:#fff0f0;color:#c62828}.restart-message{text-align:center;padding:60px 20px}@media(max-width:760px){.backup-list article{align-items:flex-start;flex-direction:column}}
|
||||
|
||||
Reference in New Issue
Block a user