<?php if (!defined('DASHBOARD_ROOT')) { http_response_code(403); exit; } RBAC::enforce('manage.backups'); $backupDir = '/var/www/backups/'; $backups = []; if (is_dir($backupDir)) { $files = glob($backupDir . '*.sql.gz'); foreach ($files as $f) { $stat = stat($f); $backups[] = ['file'=>basename($f), 'size'=>$stat['size'], 'modified'=>date('Y-m-d H:i', $stat['mtime']), 'hash'=>hash_file('sha256', $f)]; } usort($backups, fn($a,$b) => strcmp($b['modified'], $a['modified'])); } $lastVerify = Database::scalar("SELECT MAX(created_at) FROM events WHERE action='backup.verified'") ?: 'Never'; $lastBackup = !empty($backups) ? $backups[0]['modified'] : 'None found'; ?> <div id="backup-verify" class="panel"> <span class="section-label">Backup Verification</span> <div class="detail-grid"> <div><span class="kpi-label">Backup Count</span><span class="kpi-value"><?= count($backups) ?> </span></div> <div><span class="kpi-label">Latest Backup</span><span><?= UI::e($lastBackup) ?> </span></div> <div><span class="kpi-label">Last Verified</span><span><?= UI::e($lastVerify) ?> </span></div> <div><span class="kpi-label">Backup Dir</span><span class="code"><?= UI::e($backupDir) ?> </span></div> </div> <div id="backup-list" class="panel"> <span class="section-label">Available Backups</span><?php foreach ($backups as $b): ?> <div class="event-row"> <span class="event-action"><?= UI::e($b['file']) ?> </span> <span><?= UI::e(round($b['size']/1024/1024,2)) ?> MB</span> <span class="event-time"><?= UI::e($b['modified']) ?> </span> <a class="btn btn-sm verify-btn" href="javascript:void(0)" data-file="<?= UI::e($b['file']) ?> " data-hash="<?= UI::e($b['hash']) ?> "><span>Verify</span></a> <a class="btn btn-danger btn-sm restore-btn" href="javascript:void(0)" data-file="<?= UI::e($b['file']) ?> "><span>Restore</span></a> </div><?php endforeach; ?> <?php if (empty($backups)): ?> <div class="empty-state"><span>No backups found in<?= UI::e($backupDir) ?> </span></div><?php endif; ?> </div> <div class="panel"> <span class="section-label">Create Backup</span> <div class="form-group"><span>Include</span> <select id="bak-scope" class="form-group input"> <option value="full">Full (Schema + Data)</option> <option value="data">Data Only</option> </select> </div> <div class="form-actions"><a id="bak-create" class="btn btn-primary" href="javascript:void(0)"><span>Create Backup Now</span></a></div> </div> <div id="bak-log" class="term-output" style="display:none"></div> </div> <script nonce="<?= $cspNonce ?? '' ?> "> document.querySelectorAll('.verify-btn').forEach(function(b){b.addEventListener('click',function(){ var file=b.dataset.file;var hash=b.dataset.hash; window.dashFetch('verify',{action:'backup_verify','data[file]':file,'data[expected_hash]':hash},function(r){ var log=document.getElementById('bak-log');log.style.display='block'; if(r.ok){log.innerHTML='<div class="term-line" style="color:#4ade80">✓ Backup '+file+' verified. SHA-256 matches. Tables: '+(r.table_count||'?')+'</div>';} else{log.innerHTML='<div class="term-line" style="color:#f87171">✗ Verification FAILED: '+(r.error||'unknown')+'</div>';} }); });}); document.querySelectorAll('.restore-btn').forEach(function(b){b.addEventListener('click',function(){ if(!confirm('DANGER: Restore backup? This will REPLACE the current database. This action requires elevated verification.'))return; window.dashFetch('verify',{action:'backup_restore','data[file]':b.dataset.file},function(r){ var log=document.getElementById('bak-log');log.style.display='block'; log.innerHTML='<div class="term-result">'+JSON.stringify(r)+'</div>'; }); });}); document.getElementById('bak-create').addEventListener('click',function(){ window.dashFetch('verify',{action:'backup_create','data[scope]':document.getElementById('bak-scope').value},function(r){ var log=document.getElementById('bak-log');log.style.display='block'; if(r.ok){log.innerHTML='<div class="term-line" style="color:#4ade80">✓ Backup created: '+(r.filename||'')+'</div>';setTimeout(function(){location.reload();},2000);} else{log.innerHTML='<div class="term-line" style="color:#f87171">✗ '+r.error+'</div>';} }); }); </script>