mirror of
https://github.com/myronblair/tomtomgames
synced 2026-06-30 17:51:08 -05:00
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:
@@ -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) {
|
||||
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
ob_start();
|
||||
require_once __DIR__ . '/../../includes/auth.php';
|
||||
ob_end_clean();
|
||||
|
||||
$action = $_GET['action'] ?? 'list';
|
||||
|
||||
if ($action !== 'download') {
|
||||
header('Content-Type: application/json');
|
||||
}
|
||||
|
||||
if (!isLoggedIn() || empty($_SESSION['is_admin'])) {
|
||||
if ($action !== 'download') echo json_encode(['success'=>false,'error'=>'Forbidden']);
|
||||
else { http_response_code(403); echo 'Forbidden'; }
|
||||
exit;
|
||||
}
|
||||
|
||||
$backupDir = '/home/tomtomgames.com/backups';
|
||||
if (!is_dir($backupDir)) @mkdir($backupDir, 0750, true);
|
||||
|
||||
switch ($action) {
|
||||
|
||||
case 'list':
|
||||
$files = glob($backupDir . '/ttg_backup_*.zip') ?: [];
|
||||
rsort($files);
|
||||
$backups = array_map(fn($f) => [
|
||||
'name' => basename($f),
|
||||
'size' => filesize($f),
|
||||
'created' => date('Y-m-d H:i:s', filemtime($f)),
|
||||
], $files);
|
||||
echo json_encode(['success'=>true, 'backups'=>$backups]);
|
||||
break;
|
||||
|
||||
case 'create':
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false,'error'=>'POST required']); exit; }
|
||||
set_time_limit(300);
|
||||
ignore_user_abort(true);
|
||||
|
||||
$date = date('Y-m-d_H-i-s');
|
||||
$sqlFile = "/tmp/ttg_db_{$date}.sql";
|
||||
$zipFile = "{$backupDir}/ttg_backup_{$date}.zip";
|
||||
$siteDir = '/home/tomtomgames.com/public_html';
|
||||
|
||||
// Export database
|
||||
$dbCmd = sprintf(
|
||||
'/usr/bin/mysqldump -u %s -p%s %s > %s 2>&1',
|
||||
escapeshellarg(DB_USER), escapeshellarg(DB_PASS),
|
||||
escapeshellarg(DB_NAME), escapeshellarg($sqlFile)
|
||||
);
|
||||
exec($dbCmd, $dbOut, $dbRc);
|
||||
if ($dbRc !== 0 || !file_exists($sqlFile) || filesize($sqlFile) < 10) {
|
||||
@unlink($sqlFile);
|
||||
echo json_encode(['success'=>false,'error'=>'Database export failed']); exit;
|
||||
}
|
||||
|
||||
// Zip site files + db dump into one archive
|
||||
$zipCmd = sprintf(
|
||||
'/usr/bin/zip -r %s %s %s -x "*/backups/*" 2>&1',
|
||||
escapeshellarg($zipFile),
|
||||
escapeshellarg($siteDir),
|
||||
escapeshellarg($sqlFile)
|
||||
);
|
||||
exec($zipCmd, $zipOut, $zipRc);
|
||||
@unlink($sqlFile);
|
||||
|
||||
if ($zipRc !== 0 || !file_exists($zipFile)) {
|
||||
@unlink($zipFile);
|
||||
echo json_encode(['success'=>false,'error'=>'Archive creation failed']); exit;
|
||||
}
|
||||
|
||||
// Prune — keep only the 7 most recent
|
||||
$all = glob($backupDir . '/ttg_backup_*.zip') ?: [];
|
||||
rsort($all);
|
||||
foreach (array_slice($all, 7) as $old) @unlink($old);
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'name' => basename($zipFile),
|
||||
'size' => filesize($zipFile),
|
||||
'created' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
break;
|
||||
|
||||
case 'download':
|
||||
$name = basename($_GET['file'] ?? '');
|
||||
if (!preg_match('/^ttg_backup_[\d_-]+\.zip$/', $name)) {
|
||||
http_response_code(400); echo 'Invalid filename'; exit;
|
||||
}
|
||||
$path = $backupDir . '/' . $name;
|
||||
if (!file_exists($path)) { http_response_code(404); echo 'Not found'; exit; }
|
||||
|
||||
header('Content-Type: application/zip');
|
||||
header('Content-Disposition: attachment; filename="' . $name . '"');
|
||||
header('Content-Length: ' . filesize($path));
|
||||
header('Cache-Control: no-cache');
|
||||
readfile($path);
|
||||
exit;
|
||||
|
||||
case 'delete':
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false,'error'=>'POST required']); exit; }
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
$name = basename($data['name'] ?? '');
|
||||
if (!preg_match('/^ttg_backup_[\d_-]+\.zip$/', $name)) {
|
||||
echo json_encode(['success'=>false,'error'=>'Invalid filename']); exit;
|
||||
}
|
||||
$path = $backupDir . '/' . $name;
|
||||
if (file_exists($path)) @unlink($path);
|
||||
echo json_encode(['success'=>true]);
|
||||
break;
|
||||
|
||||
default:
|
||||
echo json_encode(['success'=>false,'error'=>'Unknown action']);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
#!/bin/bash
|
||||
# TomTomGames automated backup — runs daily at 2 AM via cron
|
||||
# Cron entry: 0 2 * * * /usr/local/bin/ttg-backup.sh >> /home/tomtomgames.com/backups/backup.log 2>&1
|
||||
|
||||
BACKUP_DIR="/home/tomtomgames.com/backups"
|
||||
SITE_DIR="/home/tomtomgames.com/public_html"
|
||||
DB_NAME="tomt_ttg_db"
|
||||
DB_USER="tomt_ttg_user"
|
||||
DB_PASS='q#q+mrOcozsa7I6J'
|
||||
DATE=$(date +%Y-%m-%d_%H-%M-%S)
|
||||
SQL_FILE="/tmp/ttg_db_${DATE}.sql"
|
||||
ZIP_FILE="${BACKUP_DIR}/ttg_backup_${DATE}.zip"
|
||||
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Starting backup..."
|
||||
|
||||
# Export database
|
||||
/usr/bin/mysqldump -u "$DB_USER" "-p${DB_PASS}" "$DB_NAME" > "$SQL_FILE" 2>&1
|
||||
if [ $? -ne 0 ] || [ ! -s "$SQL_FILE" ]; then
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: Database export failed"
|
||||
rm -f "$SQL_FILE"
|
||||
exit 1
|
||||
fi
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Database exported ($(du -sh "$SQL_FILE" | cut -f1))"
|
||||
|
||||
# Create zip archive (site files + db dump)
|
||||
/usr/bin/zip -r "$ZIP_FILE" "$SITE_DIR" "$SQL_FILE" -x "*/backups/*" > /dev/null 2>&1
|
||||
RC=$?
|
||||
rm -f "$SQL_FILE"
|
||||
|
||||
if [ $RC -ne 0 ] || [ ! -f "$ZIP_FILE" ]; then
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: Archive creation failed"
|
||||
rm -f "$ZIP_FILE"
|
||||
exit 1
|
||||
fi
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Archive created: $(basename "$ZIP_FILE") ($(du -sh "$ZIP_FILE" | cut -f1))"
|
||||
|
||||
# Keep only the 7 most recent backups
|
||||
ls -t "${BACKUP_DIR}"/ttg_backup_*.zip 2>/dev/null | tail -n +8 | while read old; do
|
||||
rm -f "$old"
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Pruned old backup: $(basename "$old")"
|
||||
done
|
||||
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Backup complete."
|
||||
Reference in New Issue
Block a user