From e65749c65827284c46869d00838129d50398ad30 Mon Sep 17 00:00:00 2001 From: mfwadejr <125498560+mfwadejr@users.noreply.github.com> Date: Sat, 29 Aug 2026 16:15:54 -0400 Subject: [PATCH] Add Admin database backup and restore page --- public/app.js | 13 ++++++++++++- public/extras.css | 1 + public/index.html | 2 +- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/public/app.js b/public/app.js index 0af1b26..6499912 100644 --- a/public/app.js +++ b/public/app.js @@ -4,7 +4,7 @@ const PAGE_SIZE = 10; const money = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }); const today = () => new Date().toISOString().slice(0, 10); const fmtDate = (v) => v ? new Date(`${v}T12:00:00`).toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric" }) : "—"; -const fmtDateTime = (v) => v ? new Date(`${v.replace(" ","T")}Z`).toLocaleString("en-US", { year:"numeric", month:"short", day:"numeric", hour:"numeric", minute:"2-digit" }) : "—"; +const fmtDateTime = (v) => v ? new Date(v.includes("T")?v:`${v.replace(" ","T")}Z`).toLocaleString("en-US", { year:"numeric", month:"short", day:"numeric", hour:"numeric", minute:"2-digit" }) : "—"; const esc = (v = "") => String(v).replace(/[&<>'"]/g, c => ({ "&":"&", "<":"<", ">":">", "'":"'", '"':""" })[c]); const ids = p => [["UID",p.uid],["SN",p.sn],["MAC",p.mac]].filter(([,v]) => v); const primaryId = p => ids(p)[0]?.[1] || "No identifier"; @@ -46,6 +46,8 @@ function render() { $("#revenue").textContent=monthSales.length?`${money.format(monthSales.reduce((n,p)=>n+Number(p.salePrice||0),0))} in sales`:"No sales recorded"; $("#availableBadge").textContent=available.length; $("#soldBadge").textContent=sold.length; $("#customerBadge").textContent=customers().length; document.querySelectorAll("[data-tab]").forEach(b=>b.classList.toggle("active",b.dataset.tab===state.tab)); + if(state.tab==="admin") { $(".table-wrap").hidden=true; $("#adminPanel").hidden=false; $("#pagination").innerHTML=""; renderAdmin(); return; } + $(".table-wrap").hidden=false; $("#adminPanel").hidden=true; const all=filtered(), pages=Math.max(1,Math.ceil(all.length/PAGE_SIZE)); state.page=Math.min(state.page,pages); const start=(state.page-1)*PAGE_SIZE, rows=all.slice(start,start+PAGE_SIZE); $("#thead").innerHTML=state.tab==="available"?"ProductUID / SN / MACReceivedCostStatus":state.tab==="customers"?"CustomerPhonePurchasesLast purchaseTotal spent":"ProductCustomerUID / SN / MACSoldPaymentSale price"; @@ -84,6 +86,15 @@ async function viewCustomer(p) { function restockForm(p){ openModal(`

Void sale & restock

Return ${esc(p.model)} to inventory.

`); $("[data-cancel]").onclick=closeModal; $("#restockForm").onsubmit=e=>submitForm(e,`/api/products/${encodeURIComponent(p.id)}/restock`,"Product restocked."); } async function load(){ state.products=await api("/api/products"); render(); } +async function renderAdmin(){ + const panel=$("#adminPanel"); panel.innerHTML='
Loading backups…
'; + try{const backups=await api("/api/admin/backups");panel.innerHTML=`

Database backups

Create snapshots inside the persistent /data/backups folder, download an off-server copy, or restore a previous database.

Restore replaces the active database. The app automatically creates a pre-restore backup and restarts the container.
${backups.length?backups.map(b=>`
${esc(b.name)}${fmtDateTime(b.createdAt)} · ${(b.size/1024).toFixed(1)} KB
Download
`).join(""):"

No local backups yet.

"}
`; + $("#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='

Restore complete

The container is restarting. Refresh this page in a few seconds.

';}catch(e){toast(e.message);}}); + }catch(e){panel.innerHTML=`

Unable to load backups

${esc(e.message)}

`;} +} +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='

Restore complete

The container is restarting. Refresh this page in a few seconds.

';}catch(e){storageStatus("critical","Database error");toast(e.message);}} document.querySelectorAll("[data-tab]").forEach(b=>b.onclick=()=>{state.tab=b.dataset.tab;state.page=1;render();}); $("#search").oninput=e=>{state.query=e.target.value;state.page=1;render();}; $("[data-open=receive]").onclick=receiveForm; $("[data-open=sell]").onclick=()=>sellForm(); diff --git a/public/extras.css b/public/extras.css index 85c9e8a..0d6057b 100644 --- a/public/extras.css +++ b/public/extras.css @@ -3,3 +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}} diff --git a/public/index.html b/public/index.html index 1a5dd16..a933acb 100644 --- a/public/index.html +++ b/public/index.html @@ -1,5 +1,5 @@ vSeeBox Stockroom
SStockroom

Inventory Overview

Available products0Ready to sell
Sold this month0No sales recorded
Inventory value$0Based on purchase cost
-
+