diff --git a/admin/index.php b/admin/index.php
index 77bc9a4..1eaf831 100644
--- a/admin/index.php
+++ b/admin/index.php
@@ -315,6 +315,8 @@ tr:hover td{background:rgba(255,255,255,.015)}
+
β³ Pending Signups
@@ -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 = '
Loading...
';
+ const d = await fetch('/api/backup.php?action=list').then(r=>r.json());
+ if (!d.success) { list.innerHTML='
Failed to load backups.
'; return; }
+ count.textContent = d.backups.length + ' / 7';
+ if (!d.backups.length) {
+ list.innerHTML='
No backups yet. Click Create Backup Now to make the first one.
';
+ return;
+ }
+ list.innerHTML = d.backups.map((b, i) => {
+ const sizeMB = (b.size / 1048576).toFixed(2);
+ const isLatest = i === 0;
+ return `
+
${isLatest ? 'π’' : 'πΏ'}
+
+
${escHtmlA(b.name)}
+
${escHtmlA(b.created)} Β· ${sizeMB} MB${isLatest?' Β· Latest':''}
+
+
+
`;
+ }).join('') + '
Oldest backup is automatically removed when a new one is created beyond the 7-backup limit.
';
+}
+
+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) {
diff --git a/api/backup.php b/api/backup.php
new file mode 100644
index 0000000..f32978c
--- /dev/null
+++ b/api/backup.php
@@ -0,0 +1,113 @@
+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']);
+}
diff --git a/scripts/ttg-backup.sh b/scripts/ttg-backup.sh
new file mode 100644
index 0000000..e0ebf31
--- /dev/null
+++ b/scripts/ttg-backup.sh
@@ -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."