From 625fae11ab3d71392b9d9d5ccb4aeff0e1316579 Mon Sep 17 00:00:00 2001
From: mfwadejr <125498560+mfwadejr@users.noreply.github.com>
Date: Sun, 30 Aug 2026 16:29:02 -0400
Subject: [PATCH] Move automatic scheduling into Database backups
---
public/app.js | 3 ++-
public/extras.css | 2 +-
public/theme.css | 2 +-
3 files changed, 4 insertions(+), 3 deletions(-)
diff --git a/public/app.js b/public/app.js
index 3d18384..d08516e 100644
--- a/public/app.js
+++ b/public/app.js
@@ -111,7 +111,8 @@ async function renderAdmin(){
const panel=$("#adminPanel"); panel.innerHTML='
Loading administration…
';
try{const [backups,users,audit,diagnostics]=await Promise.all([api("/api/admin/backups"),api("/api/admin/users"),api("/api/admin/audit"),api("/api/admin/diagnostics")]);panel.innerHTML=`User accounts Create administrators or read-only accounts. New and reset passwords must be changed at next login.
${users.map(u=>`
${esc(u.username)} ${u.role==="admin"?"Admin":"Read-Only"} · ${u.enabled?"Enabled":"Disabled"}${u.mustChangePassword?" · Password change required":""}${u.lastLoginAt?` · Last login ${fmtDateTime(u.lastLoginAt)}`:""}
${u.role==="admin"?"Make Read-Only":"Make Admin"} Reset password ${u.enabled?"Disable":"Enable"} Delete
`).join("")}
Database backups Create snapshots inside /data/backups, download an off-server copy, or restore a previous database.
Create backup now Restore uploaded backup
Restore replaces the active database and all user accounts. Your current password is required. Everyone will be signed out afterward.
${backups.length?backups.map(b=>`
${esc(b.name)} ${fmtDateTime(b.createdAt)} · ${(b.size/1024).toFixed(1)} KB
`).join(""):"
No local backups yet.
"}
Audit log The latest 250 security and data-changing events. Audit entries cannot be edited or deleted.
${audit.map(a=>`
${esc(a.username)} ${esc(a.action.replaceAll("_"," "))} ${fmtDateTime(a.createdAt)}${a.target?` · ${esc(a.target)}`:""}${a.details?` · ${esc(a.details)}`:""}${a.ipAddress?` · ${esc(a.ipAddress)}`:""} `).join("")||"
No audit events yet.
"}
`;
panel.querySelector(".admin-page").insertAdjacentHTML("afterbegin",'vBoxStock Administration Manage product models, access, data protection, and account activity.
');
- const bytes=n=>n>1073741824?`${(n/1073741824).toFixed(1)} GB`:n>1048576?`${(n/1048576).toFixed(1)} MB`:`${(n/1024).toFixed(1)} KB`,systemSection=document.createElement("section");systemSection.className="admin-section system-section";systemSection.innerHTML=`System and data tools vBoxStock ${esc(diagnostics.appVersion)} · Database schema ${diagnostics.schemaVersion} · ${esc(diagnostics.nodeVersion)}
Database ${bytes(diagnostics.databaseSize)} ${diagnostics.dataWritable?"/data is writable":"/data is not writable"} Storage available ${bytes(diagnostics.diskFree)} of ${bytes(diagnostics.diskTotal)} Backups ${diagnostics.backupCount} ${diagnostics.lastBackupAt?`Latest ${fmtDateTime(diagnostics.lastBackupAt)}`:"None created"} Time zone ${esc(diagnostics.timeZone)} ${esc(diagnostics.dataDirectory)} `;panel.querySelector(".admin-intro").insertAdjacentElement("afterend",systemSection);$("#backupScheduleForm").onsubmit=async e=>{e.preventDefault();const data=Object.fromEntries(new FormData(e.currentTarget));try{await api("/api/admin/backup-settings",{method:"PATCH",body:JSON.stringify({enabled:Boolean(data.enabled),hour:Number(data.hour),retention:Number(data.retention)})});toast("Backup schedule saved.");renderAdmin()}catch(error){toast(error.message)}};
+ const bytes=n=>n>1073741824?`${(n/1073741824).toFixed(1)} GB`:n>1048576?`${(n/1048576).toFixed(1)} MB`:`${(n/1024).toFixed(1)} KB`,systemSection=document.createElement("section");systemSection.className="admin-section system-section";systemSection.innerHTML=`System diagnostics vBoxStock ${esc(diagnostics.appVersion)} · Database schema ${diagnostics.schemaVersion} · ${esc(diagnostics.nodeVersion)}
Database ${bytes(diagnostics.databaseSize)} ${diagnostics.dataWritable?"/data is writable":"/data is not writable"} Storage available ${bytes(diagnostics.diskFree)} of ${bytes(diagnostics.diskTotal)} Backups ${diagnostics.backupCount} ${diagnostics.lastBackupAt?`Latest ${fmtDateTime(diagnostics.lastBackupAt)}`:"None created"} Time zone ${esc(diagnostics.timeZone)} ${esc(diagnostics.dataDirectory)} `;panel.querySelector(".admin-intro").insertAdjacentElement("afterend",systemSection);
+ const backupSection=[...panel.querySelectorAll(".admin-section")].find(section=>section.querySelector("h2")?.textContent==="Database backups"),schedule=document.createElement("div"),weekdays=["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"];schedule.className="automatic-backups";schedule.innerHTML=`Automatic backups Run a retained local backup every day or once per week. Manual, pre-upgrade, and pre-restore backups are never removed automatically.
Last automatic backup: ${diagnostics.backupSettings.lastScheduledBackup?fmtDate(diagnostics.backupSettings.lastScheduledBackup):"Not yet run"}Next automatic backup: ${diagnostics.backupSettings.nextScheduledBackup?fmtDateTime(diagnostics.backupSettings.nextScheduledBackup):"Disabled"}
`;backupSection.querySelector(".backup-warning").insertAdjacentElement("beforebegin",schedule);const frequency=$("#backupScheduleForm [name=frequency]"),weekly=$("#backupScheduleForm .weekly-field"),updateFrequency=()=>weekly.hidden=frequency.value!=="weekly";frequency.onchange=updateFrequency;updateFrequency();$("#backupScheduleForm").onsubmit=async e=>{e.preventDefault();const data=Object.fromEntries(new FormData(e.currentTarget));try{await api("/api/admin/backup-settings",{method:"PATCH",body:JSON.stringify({enabled:Boolean(data.enabled),frequency:data.frequency,weekday:Number(data.weekday),hour:Number(data.hour),retention:Number(data.retention)})});toast("Automatic backup schedule saved.");renderAdmin()}catch(error){toast(error.message)}};
const models=await api("/api/admin/models"),modelSection=document.createElement("section");modelSection.className="admin-section model-section";modelSection.innerHTML=`Product models Active models are available when receiving products. Archiving removes a model from new receiving while preserving inventory and sales history.
${models.map(m=>`
${esc(m.name)} ${m.active?"Active":"Archived"}
${m.availableCount} available · ${m.soldCount} sold ${m.totalCount===0?`Rename `:""}${m.active?"Archive":"Reactivate"} ${m.totalCount===0?`Delete `:""}
`).join("")||"
No models configured.
"}
`;panel.querySelector(".admin-intro").insertAdjacentElement("afterend",modelSection);
const finishModelChange=async message=>{state.models=await api("/api/models");toast(message);renderAdmin();};
$("#createModelForm").onsubmit=async e=>{e.preventDefault();try{await api("/api/admin/models",{method:"POST",body:JSON.stringify(Object.fromEntries(new FormData(e.currentTarget)))});await finishModelChange("Model added.");}catch(error){toast(error.message);}};
diff --git a/public/extras.css b/public/extras.css
index b67fa60..ae7de3c 100644
--- a/public/extras.css
+++ b/public/extras.css
@@ -7,4 +7,4 @@
.auth-pending #appShell,.auth-required #appShell{display:none}.role-admin #authScreen,.role-readonly #authScreen{display:none}.auth-screen{min-height:100vh;display:grid;place-items:center;padding:24px;background:linear-gradient(145deg,#eef4ff,#f8fafc)}.auth-card{width:min(430px,100%);background:#fff;border:1px solid #dfe5ef;border-radius:18px;box-shadow:0 22px 60px #17203320;padding:34px}.auth-brand{margin-bottom:28px}.auth-card h1{margin:0 0 7px}.auth-card>div>p{color:#68758b}.auth-submit{width:100%;margin-top:14px}.auth-message{background:#fff0f0;color:#a51d1d;border:1px solid #fecaca;border-radius:8px;padding:10px 12px;margin:14px 0}.auth-logout{width:100%;margin-top:10px}.user-menu{display:flex;align-items:center;gap:10px;border-left:1px solid #e3e7ef;padding-left:18px}.user-menu span{display:grid;font-size:13px}.user-menu small{color:#758197}.admin-section{display:grid;gap:16px;border-bottom:1px solid #e3e7ef;padding-bottom:28px}.admin-section:last-child{border-bottom:0}.inline-user-form{display:grid;grid-template-columns:1fr 1fr 180px auto;gap:10px;align-items:end}.inline-user-form label{margin:0}.user-list,.audit-list{border:1px solid #e3e7ef;border-radius:10px;overflow:hidden}.user-list article{display:flex;justify-content:space-between;align-items:center;gap:15px;padding:14px;border-bottom:1px solid #edf0f5}.user-list article:last-child{border-bottom:0}.user-list small{display:block;color:#7b8799;margin-top:4px}.user-list article>div:last-child{display:flex;gap:7px;flex-wrap:wrap}.delete-backup{background:#fff0f0;color:#c62828}.audit-list{max-height:420px;overflow:auto}.audit-list article{display:grid;grid-template-columns:140px 180px 1fr;gap:12px;padding:10px 13px;border-bottom:1px solid #edf0f5;font-size:13px}.audit-list span{text-transform:capitalize}.audit-list small{color:#7b8799}@media(max-width:900px){.inline-user-form{grid-template-columns:1fr 1fr}.user-list article{align-items:flex-start;flex-direction:column}.audit-list article{grid-template-columns:1fr}.user-menu span{display:none}}@media(max-width:600px){.inline-user-form{grid-template-columns:1fr}.user-menu{padding-left:5px}.user-menu button{font-size:12px;padding:8px}.auth-card{padding:25px}}
.inline-model-form{display:grid;grid-template-columns:minmax(220px,1fr) auto;gap:10px;align-items:end}.inline-model-form label{margin:0}.model-list{border:1px solid #e3e7ef;border-radius:10px;overflow:hidden}.model-list article{display:flex;justify-content:space-between;align-items:center;gap:15px;padding:14px;border-bottom:1px solid #edf0f5}.model-list article:last-child{border-bottom:0}.model-summary>div{display:flex;align-items:center;gap:9px}.model-summary small{display:block;color:#7b8799;margin-top:5px}.model-status{display:inline-flex;border-radius:99px;padding:3px 8px;font-size:11px;font-weight:750}.model-status.active{background:#dcfce7;color:#166534}.model-status.archived{background:#eef1f5;color:#526071}.model-actions{display:flex;gap:7px;flex-wrap:wrap}@media(max-width:760px){.inline-model-form{grid-template-columns:1fr}.model-list article{align-items:stretch;flex-direction:column}.model-actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr))}.model-actions button{text-align:center}}
.conditional-fields{margin:2px 0 14px;padding:16px;border:1px solid #e3e7ef;border-radius:11px;background:#f8fafc}.conditional-fields h3{margin:0 0 12px}.conditional-hint{margin:4px 0 16px;padding:13px;border:1px dashed #d7deea;border-radius:9px;color:#68758b}.field-help{display:block;margin-top:-8px;color:#68758b}.warranty-pill{display:inline-flex;align-items:center;border-radius:99px;padding:5px 9px;font-size:12px;font-weight:750;white-space:nowrap}.warranty-pill.in-warranty{background:#dcfce7;color:#166534}.warranty-pill.expired{background:#fee2e2;color:#b91c1c}.warranty-pill.neutral{background:#eef1f5;color:#526071}.inline-warranty-form{display:grid;grid-template-columns:minmax(180px,1fr) 130px 150px auto;gap:10px;align-items:end}.inline-warranty-form label{margin:0}.warranty-list{border:1px solid #e3e7ef;border-radius:10px;overflow:hidden}.warranty-list article{display:flex;justify-content:space-between;align-items:center;gap:15px;padding:14px;border-bottom:1px solid #edf0f5}.warranty-list article:last-child{border-bottom:0}@media(max-width:760px){.inline-warranty-form{grid-template-columns:1fr}.warranty-list article{align-items:stretch;flex-direction:column}}
-.filter-bar{display:flex;align-items:end;gap:9px;flex-wrap:wrap;padding:12px 16px;border-bottom:1px solid #e3e7ef}.filter-bar label{margin:0;min-width:125px;font-size:11px}.filter-bar select,.filter-bar input{padding:8px 9px;font-size:13px}.filter-bar .button,.filter-bar button{padding:9px 12px}.export-button{text-decoration:none}.customer-match{min-height:18px;margin:-7px 0 10px!important;color:#1d4ed8!important;font-size:12px}.diagnostic-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px}.diagnostic-grid article{display:grid;gap:4px;padding:14px;border:1px solid #e3e7ef;border-radius:10px}.diagnostic-grid span,.diagnostic-grid small{color:#68758b}.diagnostic-grid strong{font-size:18px}.backup-schedule{display:grid;grid-template-columns:minmax(220px,1fr) 180px 200px auto;gap:10px;align-items:end}.backup-schedule label{margin:0}.check-label{display:flex!important;align-items:center;gap:8px;min-height:42px}.check-label input{width:auto}.export-links{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.export-links .button{padding:8px 11px;text-decoration:none}@media(max-width:900px){.diagnostic-grid{grid-template-columns:1fr 1fr}.backup-schedule{grid-template-columns:1fr 1fr}}@media(max-width:600px){.filter-bar{align-items:stretch}.filter-bar label,.filter-bar .button,.filter-bar button{width:100%}.diagnostic-grid,.backup-schedule{grid-template-columns:1fr}}
+.filter-bar{display:flex;align-items:end;gap:9px;flex-wrap:wrap;padding:12px 16px;border-bottom:1px solid #e3e7ef}.filter-bar label{margin:0;min-width:125px;font-size:11px}.filter-bar select,.filter-bar input{padding:8px 9px;font-size:13px}.filter-bar .button,.filter-bar button{padding:9px 12px}.export-button{text-decoration:none}.customer-match{min-height:18px;margin:-7px 0 10px!important;color:#1d4ed8!important;font-size:12px}.diagnostic-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px}.diagnostic-grid article{display:grid;gap:4px;padding:14px;border:1px solid #e3e7ef;border-radius:10px}.diagnostic-grid span,.diagnostic-grid small{color:#68758b}.diagnostic-grid strong{font-size:18px}.automatic-backups{display:grid;gap:12px;padding:16px;border:1px solid #e3e7ef;border-radius:10px}.automatic-backups h3,.automatic-backups p{margin:0}.backup-schedule{display:grid;grid-template-columns:minmax(190px,1.3fr) repeat(4,minmax(130px,1fr)) auto;gap:10px;align-items:end}.backup-schedule label{margin:0}.weekly-field[hidden]{display:none!important}.check-label{display:flex!important;align-items:center;gap:8px;min-height:42px}.check-label input{width:auto}.backup-schedule-status{display:flex;gap:18px;flex-wrap:wrap;font-size:13px}.export-links{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.export-links .button{padding:8px 11px;text-decoration:none}@media(max-width:1100px){.backup-schedule{grid-template-columns:1fr 1fr 1fr}}@media(max-width:900px){.diagnostic-grid{grid-template-columns:1fr 1fr}.backup-schedule{grid-template-columns:1fr 1fr}}@media(max-width:600px){.filter-bar{align-items:stretch}.filter-bar label,.filter-bar .button,.filter-bar button{width:100%}.diagnostic-grid,.backup-schedule{grid-template-columns:1fr}}
diff --git a/public/theme.css b/public/theme.css
index f390991..816b73e 100644
--- a/public/theme.css
+++ b/public/theme.css
@@ -26,7 +26,7 @@ th{background:var(--surface-table);color:var(--muted-2)}th,td,.records nav,.back
.customer-notes article,.backup-list,.user-list,.model-list,.warranty-list,.audit-list{border-color:var(--border)}
.model-list article{border-color:var(--border)}.model-summary small{color:var(--muted)}.model-status.active{background:var(--green-soft);color:var(--green)}.model-status.archived{background:var(--surface-soft);color:var(--muted)}
.warranty-list article,.conditional-fields{border-color:var(--border)}.conditional-fields{background:var(--surface-soft)}.conditional-hint{border-color:var(--border-strong);color:var(--muted)}.field-help{color:var(--muted)}.warranty-pill.in-warranty{background:var(--green-soft);color:var(--green)}.warranty-pill.expired{background:var(--danger-soft);color:var(--danger)}.warranty-pill.neutral{background:var(--surface-soft);color:var(--muted)}
-.filter-bar{border-color:var(--border);background:var(--surface-soft)}.filter-bar .button{background:var(--surface);border:1px solid var(--border-strong);color:var(--text)}.customer-match{color:var(--blue-text)!important}.diagnostic-grid article{background:var(--surface-soft);border-color:var(--border)}.diagnostic-grid span,.diagnostic-grid small{color:var(--muted)}.export-links .button{background:var(--surface-soft);border:1px solid var(--border-strong);color:var(--text)}
+.filter-bar{border-color:var(--border);background:var(--surface-soft)}.filter-bar .button{background:var(--surface);border:1px solid var(--border-strong);color:var(--text)}.customer-match{color:var(--blue-text)!important}.diagnostic-grid article,.automatic-backups{background:var(--surface-soft);border-color:var(--border)}.diagnostic-grid span,.diagnostic-grid small,.automatic-backups p,.backup-schedule-status{color:var(--muted)}.export-links .button{background:var(--surface-soft);border:1px solid var(--border-strong);color:var(--text)}
.note-category{background:var(--blue-soft);color:var(--blue-text)}.backup-warning{background:var(--warning-soft);border-color:color-mix(in srgb,var(--warning) 40%,transparent);color:var(--warning)}
.auth-screen{background:linear-gradient(145deg,color-mix(in srgb,var(--blue) 10%,var(--page)),var(--page))}.auth-card{box-shadow:0 22px 60px var(--shadow)}
dialog{max-height:min(90dvh,900px);overflow:auto}dialog::backdrop{background:#050914aa}.close{color:var(--muted)}