Add automated backup system

- api/backup.php: list/create/download/delete backups; streams zip directly
  for downloads; 7-backup rolling prune on each create
- Each backup is a single zip containing all of public_html + a full
  mysqldump of tomt_ttg_db
- Cron at 2 AM daily via /usr/local/bin/ttg-backup.sh (already installed)
- Admin UI: 💾 Backups nav item under System section; shows backup list
  with date/size, Download + Delete per row; Create Backup Now button
  with live status; auto-loads when section is opened

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-05 22:14:32 +00:00
parent c9cf26edca
commit f5a72c55f5
3 changed files with 252 additions and 0 deletions
+95
View File
@@ -315,6 +315,8 @@ tr:hover td{background:rgba(255,255,255,.015)}
<button class="nav-item" onclick="showSec('cashout-methods')">💸 Cashout Methods</button>
<button class="nav-item" onclick="showSec('payout-settings')">💰 Payout Settings</button>
<button class="nav-item" onclick="showSec('history')">📋 All History</button>
<div class="nav-section">System</div>
<button class="nav-item" onclick="showSec('backups')">💾 Backups</button>
<div class="sidebar-footer">
<button class="logout-btn" onclick="fetch('/api/logout.php').then(()=>location='/admin/login.php')">🚪 LOGOUT</button>
</div>
@@ -978,6 +980,32 @@ tr:hover td{background:rgba(255,255,255,.015)}
<div id="hist-pagination" style="display:flex;gap:8px;justify-content:center;margin-top:16px;flex-wrap:wrap"></div>
</div>
<!-- ── BACKUPS ──────────────────────────────────────────── -->
<div class="section" id="section-backups">
<div class="page-title">💾 Backup System</div>
<div style="background:rgba(0,229,255,.05);border:1px solid rgba(0,229,255,.15);border-radius:10px;padding:12px 18px;margin-bottom:18px;display:flex;align-items:center;gap:14px;flex-wrap:wrap">
<div style="flex:1;min-width:200px">
<div style="font-size:14px;font-weight:700;color:var(--cyan);margin-bottom:2px">🕐 Automated Schedule</div>
<div style="font-size:14px;color:var(--text2)">Daily at 2:00 AM · 7 rolling backups · Files + full database export</div>
</div>
<div style="display:flex;gap:8px">
<button class="btn btn-gold" id="backup-create-btn" onclick="createBackup()" style="padding:10px 20px;font-size:15px">📦 Create Backup Now</button>
<button onclick="loadBackups()" style="background:none;border:1px solid var(--border);color:var(--text2);border-radius:var(--rs);padding:10px 14px;cursor:pointer;font-family:'Exo 2',sans-serif;font-weight:700;font-size:14px">↻</button>
</div>
</div>
<div id="backup-alert" class="alert" style="margin-bottom:12px"></div>
<div class="card" style="padding:0;overflow:hidden">
<div style="padding:14px 18px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between">
<div style="font-family:'Exo 2',sans-serif;font-weight:700;font-size:14px">Available Backups <span style="color:var(--text2);font-weight:400;font-size:13px">(last 7 days)</span></div>
<div id="backup-count" style="font-size:13px;color:var(--text2)"></div>
</div>
<div id="backup-list"><div style="padding:24px;text-align:center;color:var(--text2);font-size:15px">Loading...</div></div>
</div>
</div>
<!-- PENDING SIGNUPS -->
<div class="section" id="section-pending">
<div class="page-title">⏳ Pending Signups</div>
@@ -3341,6 +3369,72 @@ async function jumpToPurchase(purchaseId) {
}
}
// ── BACKUP SYSTEM ────────────────────────────────────────────
async function loadBackups() {
const list = document.getElementById('backup-list');
const count = document.getElementById('backup-count');
list.innerHTML = '<div style="padding:24px;text-align:center;color:var(--text2);font-size:15px">Loading...</div>';
const d = await fetch('/api/backup.php?action=list').then(r=>r.json());
if (!d.success) { list.innerHTML='<div style="padding:24px;text-align:center;color:var(--red)">Failed to load backups.</div>'; return; }
count.textContent = d.backups.length + ' / 7';
if (!d.backups.length) {
list.innerHTML='<div style="padding:32px;text-align:center;color:var(--text2);font-size:15px">No backups yet. Click <strong>Create Backup Now</strong> to make the first one.</div>';
return;
}
list.innerHTML = d.backups.map((b, i) => {
const sizeMB = (b.size / 1048576).toFixed(2);
const isLatest = i === 0;
return `<div style="display:flex;align-items:center;gap:14px;padding:14px 18px;border-bottom:1px solid var(--border);flex-wrap:wrap;${isLatest?'background:rgba(0,229,255,.025)':''}">
<div style="font-size:22px;flex-shrink:0">${isLatest ? '🟢' : '💿'}</div>
<div style="flex:1;min-width:160px">
<div style="font-family:'Exo 2',sans-serif;font-weight:700;font-size:14px;color:var(--text)">${escHtmlA(b.name)}</div>
<div style="font-size:13px;color:var(--text2);margin-top:2px">${escHtmlA(b.created)} · ${sizeMB} MB${isLatest?' · <span style="color:var(--cyan);font-weight:700">Latest</span>':''}</div>
</div>
<div style="display:flex;gap:8px;flex-shrink:0">
<a href="/api/backup.php?action=download&file=${encodeURIComponent(b.name)}"
style="background:rgba(0,229,255,.1);border:1px solid rgba(0,229,255,.25);color:var(--cyan);border-radius:var(--rs);padding:7px 14px;font-family:'Exo 2',sans-serif;font-weight:700;font-size:13px;text-decoration:none;white-space:nowrap">
⬇ Download
</a>
<button onclick="deleteBackup('${escHtmlA(b.name)}')"
style="background:rgba(255,68,68,.08);border:1px solid rgba(255,68,68,.2);color:var(--red);border-radius:var(--rs);padding:7px 12px;font-family:'Exo 2',sans-serif;font-weight:700;font-size:13px;cursor:pointer">
🗑
</button>
</div>
</div>`;
}).join('') + '<div style="padding:10px 18px;font-size:13px;color:var(--text2);border-top:1px solid var(--border)">Oldest backup is automatically removed when a new one is created beyond the 7-backup limit.</div>';
}
async function createBackup() {
const btn = document.getElementById('backup-create-btn');
const al = document.getElementById('backup-alert');
btn.disabled = true;
btn.textContent = '⏳ Creating...';
al.className = 'alert';
showAdminAlert(al, 'Creating backup — this may take up to 30 seconds…', 'info');
try {
const d = await fetch('/api/backup.php?action=create', {method:'POST'}).then(r=>r.json());
if (d.success) {
const sizeMB = (d.size / 1048576).toFixed(2);
showAdminAlert(al, `✅ Backup created: ${d.name} (${sizeMB} MB)`, 'success');
loadBackups();
loadPlatformStats();
} else {
showAdminAlert(al, '❌ ' + (d.error || 'Backup failed'), 'error');
}
} catch(e) {
showAdminAlert(al, '❌ Request failed — server may have timed out', 'error');
}
btn.disabled = false;
btn.textContent = '📦 Create Backup Now';
}
async function deleteBackup(name) {
if (!confirm(`Delete backup "${name}"?\nThis cannot be undone.`)) return;
const d = await fetch('/api/backup.php?action=delete', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({name})}).then(r=>r.json());
if (d.success) { toast('Backup deleted', 'ok'); loadBackups(); }
else toast(d.error || 'Delete failed', 'err');
}
// Sync hex input with color picker
document.addEventListener('DOMContentLoaded', function() {
const picker = document.getElementById('gf-color');
@@ -3533,6 +3627,7 @@ function showSec(name) {
if (name === 'payments') loadPaymentSettings();
if (name === 'payout-settings') loadPayoutSettings();
if (name === 'cashout-methods') loadCashoutMethods();
if (name === 'backups') loadBackups();
}
async function loadChatInbox(silent) {