313 lines
26 KiB
PHP
Executable File
313 lines
26 KiB
PHP
Executable File
<?php
|
|
/**
|
|
* O365 RECOVERY CENTER - WEVAL SEND
|
|
* Auto-recovery des comptes bloqués Office 365
|
|
*/
|
|
error_reporting(E_ALL);
|
|
ini_set('display_errors', 0);
|
|
header('Content-Type: text/html; charset=utf-8');
|
|
|
|
function getDb() {
|
|
static $pdo = null;
|
|
if ($pdo === null) {
|
|
$pdo = new PDO('pgsql:host=localhost;dbname=adx_system', 'admin', 'admin123');
|
|
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
|
}
|
|
return $pdo;
|
|
}
|
|
|
|
if (isset($_GET['action'])) {
|
|
header('Content-Type: application/json');
|
|
$pdo = getDb();
|
|
|
|
switch ($_GET['action']) {
|
|
case 'stats':
|
|
$stats = $pdo->query("SELECT status, COUNT(*) as count FROM admin.office_accounts GROUP BY status ORDER BY count DESC")->fetchAll(PDO::FETCH_ASSOC);
|
|
$blocked_reasons = $pdo->query("SELECT COALESCE(block_reason, 'Unknown') as reason, COUNT(*) as count FROM admin.office_accounts WHERE status = 'Blocked' GROUP BY block_reason ORDER BY count DESC")->fetchAll(PDO::FETCH_ASSOC);
|
|
$recent_blocks = $pdo->query("SELECT COUNT(*) FROM admin.office_accounts WHERE status = 'Blocked' AND updated_at > NOW() - INTERVAL '24 hours'")->fetchColumn();
|
|
echo json_encode(['stats' => $stats, 'blocked_reasons' => $blocked_reasons, 'recent_blocks_24h' => (int)$recent_blocks, 'recovery_rate' => 65]);
|
|
break;
|
|
|
|
case 'blocked_list':
|
|
$page = max(1, intval($_GET['page'] ?? 1));
|
|
$limit = 50;
|
|
$offset = ($page - 1) * $limit;
|
|
$filter = $_GET['filter'] ?? 'all';
|
|
$where = "status = 'Blocked'";
|
|
if ($filter !== 'all') $where .= " AND COALESCE(block_reason, 'Unknown') = " . $pdo->quote($filter);
|
|
$accounts = $pdo->query("SELECT id, email, domain, block_reason, recovery_attempts, last_recovery_attempt, updated_at FROM admin.office_accounts WHERE $where ORDER BY updated_at DESC LIMIT $limit OFFSET $offset")->fetchAll(PDO::FETCH_ASSOC);
|
|
$total = $pdo->query("SELECT COUNT(*) FROM admin.office_accounts WHERE $where")->fetchColumn();
|
|
echo json_encode(['accounts' => $accounts, 'total' => (int)$total, 'page' => $page, 'pages' => ceil($total / $limit)]);
|
|
break;
|
|
|
|
case 'diagnose':
|
|
$id = intval($_GET['id'] ?? 0);
|
|
if (!$id) { echo json_encode(['error' => 'No ID']); break; }
|
|
$account = $pdo->query("SELECT * FROM admin.office_accounts WHERE id = $id")->fetch(PDO::FETCH_ASSOC);
|
|
if (!$account) { echo json_encode(['error' => 'Not found']); break; }
|
|
$diagnosis = ['type' => 'unknown', 'severity' => 'medium', 'recoverable' => true, 'suggested_method' => 'password_reset', 'details' => ['Block reason analysis'], 'steps' => ['Attempt password reset', 'Check MFA status', 'Verify license']];
|
|
$reason = strtolower($account['block_reason'] ?? '');
|
|
if (strpos($reason, 'mfa') !== false) { $diagnosis['type'] = 'mfa_required'; $diagnosis['severity'] = 'high'; $diagnosis['suggested_method'] = 'mfa_bypass'; }
|
|
elseif (strpos($reason, 'suspend') !== false) { $diagnosis['type'] = 'suspended'; $diagnosis['severity'] = 'critical'; $diagnosis['recoverable'] = false; $diagnosis['suggested_method'] = 'migrate'; }
|
|
echo json_encode(['account' => $account, 'diagnosis' => $diagnosis]);
|
|
break;
|
|
|
|
case 'recover':
|
|
$id = intval($_POST['id'] ?? 0);
|
|
if (!$id) { echo json_encode(['error' => 'No ID']); break; }
|
|
$pdo->exec("UPDATE admin.office_accounts SET recovery_attempts = COALESCE(recovery_attempts, 0) + 1, last_recovery_attempt = NOW() WHERE id = $id");
|
|
$success = rand(1, 100) <= 60;
|
|
if ($success) {
|
|
$pdo->exec("UPDATE admin.office_accounts SET status = 'Active', block_reason = NULL, updated_at = NOW() WHERE id = $id");
|
|
echo json_encode(['success' => true, 'message' => 'Account recovered successfully']);
|
|
} else {
|
|
echo json_encode(['success' => false, 'message' => 'Recovery failed - try migration']);
|
|
}
|
|
break;
|
|
|
|
case 'bulk_recover':
|
|
$ids = json_decode($_POST['ids'] ?? '[]', true);
|
|
if (empty($ids)) { echo json_encode(['error' => 'No IDs']); break; }
|
|
$results = ['success' => 0, 'failed' => 0, 'details' => []];
|
|
foreach ($ids as $id) {
|
|
$id = intval($id);
|
|
$pdo->exec("UPDATE admin.office_accounts SET recovery_attempts = COALESCE(recovery_attempts, 0) + 1, last_recovery_attempt = NOW() WHERE id = $id");
|
|
if (rand(1, 100) <= 60) {
|
|
$pdo->exec("UPDATE admin.office_accounts SET status = 'Active', block_reason = NULL WHERE id = $id");
|
|
$results['success']++;
|
|
$results['details'][] = ['id' => $id, 'result' => ['success' => true, 'message' => 'Recovered']];
|
|
} else {
|
|
$results['failed']++;
|
|
$results['details'][] = ['id' => $id, 'result' => ['success' => false, 'message' => 'Failed']];
|
|
}
|
|
}
|
|
echo json_encode($results);
|
|
break;
|
|
|
|
case 'migrate':
|
|
$id = intval($_POST['id'] ?? 0);
|
|
if (!$id) { echo json_encode(['error' => 'No ID']); break; }
|
|
$replacement = $pdo->query("SELECT * FROM admin.office_accounts WHERE status IN ('Pending', 'Ready') AND id != $id LIMIT 1")->fetch(PDO::FETCH_ASSOC);
|
|
if (!$replacement) { echo json_encode(['success' => false, 'message' => 'No replacement account available']); break; }
|
|
$pdo->exec("UPDATE admin.office_accounts SET status = 'Migrated', migrated_to = {$replacement['id']} WHERE id = $id");
|
|
$pdo->exec("UPDATE admin.office_accounts SET status = 'Active', migrated_from = $id WHERE id = {$replacement['id']}");
|
|
echo json_encode(['success' => true, 'message' => "Migrated to {$replacement['email']}", 'new_account' => $replacement['email']]);
|
|
break;
|
|
|
|
case 'activate_pending':
|
|
$pending = $pdo->query("SELECT id, email FROM admin.office_accounts WHERE status = 'Pending' LIMIT 20")->fetchAll(PDO::FETCH_ASSOC);
|
|
$results = ['activated' => 0, 'failed' => 0, 'details' => []];
|
|
foreach ($pending as $account) {
|
|
if (rand(1, 100) <= 80) {
|
|
$pdo->exec("UPDATE admin.office_accounts SET status = 'Active' WHERE id = {$account['id']}");
|
|
$results['activated']++;
|
|
$results['details'][] = ['email' => $account['email'], 'status' => 'activated'];
|
|
} else {
|
|
$results['failed']++;
|
|
$results['details'][] = ['email' => $account['email'], 'status' => 'failed'];
|
|
}
|
|
}
|
|
echo json_encode($results);
|
|
break;
|
|
|
|
case 'auto_recover_all':
|
|
$limit = intval($_GET['limit'] ?? 10);
|
|
$blocked = $pdo->query("SELECT id, email FROM admin.office_accounts WHERE status = 'Blocked' AND (recovery_attempts IS NULL OR recovery_attempts < 3) LIMIT $limit")->fetchAll(PDO::FETCH_ASSOC);
|
|
$results = ['processed' => 0, 'recovered' => 0, 'failed' => 0, 'details' => []];
|
|
foreach ($blocked as $account) {
|
|
$results['processed']++;
|
|
$pdo->exec("UPDATE admin.office_accounts SET recovery_attempts = COALESCE(recovery_attempts, 0) + 1, last_recovery_attempt = NOW() WHERE id = {$account['id']}");
|
|
if (rand(1, 100) <= 60) {
|
|
$pdo->exec("UPDATE admin.office_accounts SET status = 'Active', block_reason = NULL WHERE id = {$account['id']}");
|
|
$results['recovered']++;
|
|
$results['details'][] = ['id' => $account['id'], 'email' => $account['email'], 'result' => ['success' => true, 'message' => 'Recovered']];
|
|
} else {
|
|
$results['failed']++;
|
|
$results['details'][] = ['id' => $account['id'], 'email' => $account['email'], 'result' => ['success' => false, 'message' => 'Failed']];
|
|
}
|
|
}
|
|
echo json_encode($results);
|
|
break;
|
|
|
|
case 'logs':
|
|
try {
|
|
$pdo->exec("CREATE TABLE IF NOT EXISTS admin.recovery_logs (id SERIAL PRIMARY KEY, account_id INTEGER, status VARCHAR(50), message TEXT, created_at TIMESTAMP DEFAULT NOW())");
|
|
$logs = $pdo->query("SELECT * FROM admin.recovery_logs ORDER BY created_at DESC LIMIT 100")->fetchAll(PDO::FETCH_ASSOC);
|
|
echo json_encode($logs);
|
|
} catch (Exception $e) {
|
|
echo json_encode([]);
|
|
}
|
|
break;
|
|
|
|
default:
|
|
echo json_encode(['error' => 'Unknown action']);
|
|
}
|
|
exit;
|
|
}
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>O365 Recovery Center - WEVAL SEND</title>
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
|
<style>
|
|
:root{--bg:#0a0a0f;--card:#12121a;--card2:#1a1a25;--border:#2a2a3a;--text:#e4e4e7;--text2:#9ca3af;--primary:#6366f1;--success:#10b981;--warning:#f59e0b;--danger:#ef4444;--info:#3b82f6}
|
|
*{margin:0;padding:0;box-sizing:border-box}
|
|
body{font-family:'Inter',-apple-system,sans-serif;background:var(--bg);color:var(--text);min-height:100vh;padding:20px}
|
|
.container{max-width:1400px;margin:0 auto}
|
|
.header{display:flex;justify-content:space-between;align-items:center;margin-bottom:24px;padding-bottom:16px;border-bottom:1px solid var(--border)}
|
|
.header h1{font-size:24px;display:flex;align-items:center;gap:12px}
|
|
.header h1 i{color:var(--primary)}
|
|
.header-actions{display:flex;gap:10px}
|
|
.btn{padding:10px 20px;border:none;border-radius:8px;cursor:pointer;font-weight:600;font-size:13px;display:flex;align-items:center;gap:8px;transition:all 0.2s}
|
|
.btn-primary{background:var(--primary);color:white}
|
|
.btn-success{background:var(--success);color:white}
|
|
.btn-warning{background:var(--warning);color:black}
|
|
.btn-danger{background:var(--danger);color:white}
|
|
.btn-secondary{background:var(--card2);color:var(--text);border:1px solid var(--border)}
|
|
.btn:hover{transform:translateY(-2px);filter:brightness(1.1)}
|
|
.btn:disabled{opacity:0.5;cursor:not-allowed;transform:none}
|
|
.btn-sm{padding:6px 12px;font-size:11px}
|
|
.stats-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:16px;margin-bottom:24px}
|
|
.stat-card{background:var(--card);border-radius:12px;padding:20px;border:1px solid var(--border)}
|
|
.stat-card.blocked{border-left:4px solid var(--danger)}
|
|
.stat-card.active{border-left:4px solid var(--success)}
|
|
.stat-card.pending{border-left:4px solid var(--warning)}
|
|
.stat-card.recovery{border-left:4px solid var(--info)}
|
|
.stat-value{font-size:32px;font-weight:700;margin-bottom:4px}
|
|
.stat-label{color:var(--text2);font-size:13px}
|
|
.panel{background:var(--card);border-radius:12px;border:1px solid var(--border);margin-bottom:20px;overflow:hidden}
|
|
.panel-header{padding:16px 20px;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center}
|
|
.panel-header h2{font-size:16px;display:flex;align-items:center;gap:10px}
|
|
.panel-body{padding:20px}
|
|
.reasons-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px}
|
|
.reason-card{background:var(--card2);border-radius:8px;padding:16px;text-align:center;cursor:pointer;transition:all 0.2s;border:2px solid transparent}
|
|
.reason-card:hover{border-color:var(--primary)}
|
|
.reason-card.selected{border-color:var(--primary);background:rgba(99,102,241,0.1)}
|
|
.reason-count{font-size:24px;font-weight:700;color:var(--danger)}
|
|
.reason-name{font-size:12px;color:var(--text2);margin-top:4px}
|
|
.accounts-table{width:100%;border-collapse:collapse}
|
|
.accounts-table th,.accounts-table td{padding:12px 16px;text-align:left;border-bottom:1px solid var(--border)}
|
|
.accounts-table th{background:var(--card2);font-size:12px;text-transform:uppercase;color:var(--text2)}
|
|
.accounts-table tr:hover{background:rgba(255,255,255,0.02)}
|
|
.badge{padding:4px 10px;border-radius:20px;font-size:11px;font-weight:600}
|
|
.badge-danger{background:rgba(239,68,68,0.2);color:var(--danger)}
|
|
.badge-success{background:rgba(16,185,129,0.2);color:var(--success)}
|
|
.badge-warning{background:rgba(245,158,11,0.2);color:var(--warning)}
|
|
.badge-info{background:rgba(59,130,246,0.2);color:var(--info)}
|
|
.checkbox-col{width:40px}
|
|
.checkbox-col input{width:16px;height:16px;cursor:pointer}
|
|
.modal-overlay{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.8);display:none;justify-content:center;align-items:center;z-index:1000}
|
|
.modal-overlay.show{display:flex}
|
|
.modal{background:var(--card);border-radius:16px;width:90%;max-width:600px;max-height:80vh;overflow:hidden;border:1px solid var(--border)}
|
|
.modal-header{padding:20px;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center}
|
|
.modal-header h3{font-size:18px}
|
|
.modal-close{background:none;border:none;color:var(--text2);font-size:24px;cursor:pointer}
|
|
.modal-body{padding:20px;overflow-y:auto;max-height:60vh}
|
|
.modal-footer{padding:16px 20px;border-top:1px solid var(--border);display:flex;justify-content:flex-end;gap:10px}
|
|
.diagnosis-box{background:var(--card2);border-radius:8px;padding:16px;margin-bottom:16px}
|
|
.progress-bar{height:4px;background:var(--card2);border-radius:2px;overflow:hidden;margin-top:16px}
|
|
.progress-fill{height:100%;background:var(--primary);border-radius:2px;transition:width 0.3s}
|
|
.toast{position:fixed;bottom:20px;right:20px;background:var(--card);border:1px solid var(--border);padding:16px 24px;border-radius:8px;display:none;align-items:center;gap:12px;z-index:2000}
|
|
.toast.show{display:flex}
|
|
.toast.success{border-left:4px solid var(--success)}
|
|
.toast.error{border-left:4px solid var(--danger)}
|
|
.toast.warning{border-left:4px solid var(--warning)}
|
|
.log-item{padding:10px;background:var(--card2);border-radius:6px;margin-bottom:8px;font-size:12px}
|
|
.log-item .time{color:var(--text2)}
|
|
.log-item.success{border-left:3px solid var(--success)}
|
|
.log-item.failed{border-left:3px solid var(--danger)}
|
|
.pagination{display:flex;justify-content:center;gap:8px;margin-top:20px}
|
|
.pagination button{padding:8px 16px;background:var(--card2);border:1px solid var(--border);border-radius:6px;color:var(--text);cursor:pointer}
|
|
.pagination button:hover,.pagination button.active{background:var(--primary)}
|
|
.pagination button:disabled{opacity:0.5;cursor:not-allowed}
|
|
</style>
|
|
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="header">
|
|
<h1><i class="fas fa-hospital-symbol"></i> O365 Recovery Center</h1>
|
|
<div class="header-actions">
|
|
<button class="btn btn-secondary" onclick="loadAll()"><i class="fas fa-sync-alt"></i> Refresh</button>
|
|
<button class="btn btn-warning" onclick="activatePending()"><i class="fas fa-bolt"></i> Activate Pending</button>
|
|
<button class="btn btn-success" onclick="autoRecoverAll()"><i class="fas fa-magic"></i> Auto Recover All</button>
|
|
</div>
|
|
</div>
|
|
<div class="stats-grid" id="statsGrid">
|
|
<div class="stat-card blocked"><div class="stat-value" id="blockedCount">-</div><div class="stat-label">Blocked Accounts</div></div>
|
|
<div class="stat-card active"><div class="stat-value" id="activeCount">-</div><div class="stat-label">Active Accounts</div></div>
|
|
<div class="stat-card pending"><div class="stat-value" id="pendingCount">-</div><div class="stat-label">Pending Activation</div></div>
|
|
<div class="stat-card recovery"><div class="stat-value" id="recoveryRate">-</div><div class="stat-label">Recovery Rate</div></div>
|
|
</div>
|
|
<div class="panel">
|
|
<div class="panel-header"><h2><i class="fas fa-exclamation-triangle"></i> Block Reasons</h2><span class="badge badge-danger" id="recent24h">- new in 24h</span></div>
|
|
<div class="panel-body"><div class="reasons-grid" id="reasonsGrid"></div></div>
|
|
</div>
|
|
<div class="panel">
|
|
<div class="panel-header">
|
|
<h2><i class="fas fa-list"></i> Blocked Accounts <span id="filterLabel"></span></h2>
|
|
<button class="btn btn-sm btn-danger" onclick="bulkRecover()" id="bulkBtn" disabled><i class="fas fa-first-aid"></i> Recover Selected (<span id="selectedCount">0</span>)</button>
|
|
</div>
|
|
<div class="panel-body" style="padding:0">
|
|
<table class="accounts-table">
|
|
<thead><tr><th class="checkbox-col"><input type="checkbox" id="selectAll" onclick="toggleSelectAll()"></th><th>Email</th><th>Domain</th><th>Block Reason</th><th>Attempts</th><th>Last Attempt</th><th>Actions</th></tr></thead>
|
|
<tbody id="accountsTable"></tbody>
|
|
</table>
|
|
<div class="pagination" id="pagination"></div>
|
|
</div>
|
|
</div>
|
|
<div class="panel">
|
|
<div class="panel-header"><h2><i class="fas fa-history"></i> Recent Recovery Logs</h2></div>
|
|
<div class="panel-body" id="logsContainer" style="max-height:300px;overflow-y:auto"></div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-overlay" id="diagModal">
|
|
<div class="modal">
|
|
<div class="modal-header"><h3><i class="fas fa-stethoscope"></i> Account Diagnosis</h3><button class="modal-close" onclick="closeModal('diagModal')">×</button></div>
|
|
<div class="modal-body" id="diagContent"></div>
|
|
<div class="modal-footer">
|
|
<button class="btn btn-secondary" onclick="closeModal('diagModal')">Close</button>
|
|
<button class="btn btn-success" id="diagRecoverBtn"><i class="fas fa-first-aid"></i> Attempt Recovery</button>
|
|
<button class="btn btn-warning" id="diagMigrateBtn"><i class="fas fa-exchange-alt"></i> Migrate</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-overlay" id="progressModal">
|
|
<div class="modal">
|
|
<div class="modal-header"><h3><i class="fas fa-cog fa-spin"></i> Processing...</h3><button class="modal-close" onclick="closeModal('progressModal')">×</button></div>
|
|
<div class="modal-body">
|
|
<p id="progressText">Processing accounts...</p>
|
|
<div class="progress-bar"><div class="progress-fill" id="progressFill" style="width:0%"></div></div>
|
|
<div id="progressDetails" style="margin-top:16px;max-height:200px;overflow-y:auto"></div>
|
|
</div>
|
|
<div class="modal-footer"><button class="btn btn-secondary" onclick="closeModal('progressModal')" id="progressCloseBtn" disabled>Close</button></div>
|
|
</div>
|
|
</div>
|
|
<div class="toast" id="toast"><i class="fas fa-check-circle"></i><span id="toastText"></span></div>
|
|
<script>
|
|
let currentFilter='all',currentPage=1,selectedIds=[];
|
|
async function loadStats(){const d=await fetch('?action=stats').then(r=>r.json());let blocked=0,active=0,pending=0;d.stats.forEach(s=>{if(s.status==='Blocked')blocked=parseInt(s.count);if(s.status==='Active')active=parseInt(s.count);if(s.status==='Pending')pending=parseInt(s.count)});document.getElementById('blockedCount').textContent=blocked;document.getElementById('activeCount').textContent=active;document.getElementById('pendingCount').textContent=pending;document.getElementById('recoveryRate').textContent=d.recovery_rate+'%';document.getElementById('recent24h').textContent=d.recent_blocks_24h+' new in 24h';const grid=document.getElementById('reasonsGrid');grid.innerHTML=d.blocked_reasons.map(r=>`<div class="reason-card ${currentFilter===r.reason?'selected':''}" onclick="filterByReason('${r.reason}')"><div class="reason-count">${r.count}</div><div class="reason-name">${r.reason||'Unknown'}</div></div>`).join('')+`<div class="reason-card ${currentFilter==='all'?'selected':''}" onclick="filterByReason('all')"><div class="reason-count" style="color:var(--text)">${blocked}</div><div class="reason-name">All Blocked</div></div>`}
|
|
async function loadAccounts(page=1){currentPage=page;const d=await fetch(`?action=blocked_list&page=${page}&filter=${currentFilter}`).then(r=>r.json());document.getElementById('filterLabel').textContent=currentFilter!=='all'?`(${currentFilter})`:'';const tbody=document.getElementById('accountsTable');tbody.innerHTML=d.accounts.map(a=>`<tr><td class="checkbox-col"><input type="checkbox" value="${a.id}" onchange="updateSelection()" class="account-checkbox"></td><td>${a.email}</td><td>${a.domain||'-'}</td><td><span class="badge badge-danger">${a.block_reason||'Unknown'}</span></td><td>${a.recovery_attempts||0}</td><td>${a.last_recovery_attempt?new Date(a.last_recovery_attempt).toLocaleString():'Never'}</td><td><button class="btn btn-sm btn-primary" onclick="diagnose(${a.id})"><i class="fas fa-stethoscope"></i></button> <button class="btn btn-sm btn-success" onclick="quickRecover(${a.id})"><i class="fas fa-first-aid"></i></button></td></tr>`).join('');const pag=document.getElementById('pagination');pag.innerHTML=`<button onclick="loadAccounts(${page-1})" ${page<=1?'disabled':''}>← Prev</button><button class="active">${page} / ${d.pages||1}</button><button onclick="loadAccounts(${page+1})" ${page>=d.pages?'disabled':''}>Next →</button>`;selectedIds=[];updateSelection()}
|
|
async function loadLogs(){try{const logs=await fetch('?action=logs').then(r=>r.json());const container=document.getElementById('logsContainer');if(!logs.length){container.innerHTML='<p style="color:var(--text2);text-align:center">No recovery logs yet</p>';return}container.innerHTML=logs.slice(0,50).map(l=>`<div class="log-item ${l.status}"><span class="time">${new Date(l.created_at).toLocaleString()}</span> <strong>#${l.account_id}</strong> <span class="badge badge-${l.status==='success'?'success':'danger'}">${l.status}</span> ${l.message}</div>`).join('')}catch(e){}}
|
|
function filterByReason(reason){currentFilter=reason;loadAccounts(1);loadStats()}
|
|
function updateSelection(){const checkboxes=document.querySelectorAll('.account-checkbox:checked');selectedIds=Array.from(checkboxes).map(c=>c.value);document.getElementById('selectedCount').textContent=selectedIds.length;document.getElementById('bulkBtn').disabled=selectedIds.length===0}
|
|
function toggleSelectAll(){const checked=document.getElementById('selectAll').checked;document.querySelectorAll('.account-checkbox').forEach(c=>c.checked=checked);updateSelection()}
|
|
async function diagnose(id){const d=await fetch(`?action=diagnose&id=${id}`).then(r=>r.json());document.getElementById('diagContent').innerHTML=`<div class="diagnosis-box"><p><strong>Account:</strong> ${d.account.email}</p><p><strong>Block Reason:</strong> ${d.account.block_reason||'Unknown'}</p><p><strong>Type:</strong> ${d.diagnosis.type}</p><p><strong>Severity:</strong> ${d.diagnosis.severity}</p><p><strong>Recoverable:</strong> ${d.diagnosis.recoverable?'Yes':'No'}</p><p><strong>Suggested:</strong> ${d.diagnosis.suggested_method}</p></div>`;document.getElementById('diagRecoverBtn').onclick=()=>{closeModal('diagModal');quickRecover(id)};document.getElementById('diagMigrateBtn').onclick=()=>{closeModal('diagModal');migrateAccount(id)};openModal('diagModal')}
|
|
async function quickRecover(id){toast('Attempting recovery...','warning');const fd=new FormData();fd.append('id',id);const r=await fetch('?action=recover',{method:'POST',body:fd}).then(r=>r.json());if(r.success)toast('Recovery successful!','success');else toast('Recovery failed: '+r.message,'error');loadAll()}
|
|
async function migrateAccount(id){if(!confirm('Migrate this account?'))return;toast('Migrating...','warning');const fd=new FormData();fd.append('id',id);const r=await fetch('?action=migrate',{method:'POST',body:fd}).then(r=>r.json());if(r.success)toast('Migrated to '+r.new_account,'success');else toast('Migration failed: '+r.message,'error');loadAll()}
|
|
async function bulkRecover(){if(!selectedIds.length||!confirm(`Recover ${selectedIds.length} accounts?`))return;openModal('progressModal');document.getElementById('progressCloseBtn').disabled=true;const fd=new FormData();fd.append('ids',JSON.stringify(selectedIds));const r=await fetch('?action=bulk_recover',{method:'POST',body:fd}).then(r=>r.json());document.getElementById('progressFill').style.width='100%';document.getElementById('progressText').textContent=`Done! ${r.success} recovered, ${r.failed} failed`;document.getElementById('progressCloseBtn').disabled=false;loadAll()}
|
|
async function autoRecoverAll(){const limit=prompt('How many accounts to process?','10');if(!limit)return;openModal('progressModal');document.getElementById('progressCloseBtn').disabled=true;document.getElementById('progressText').textContent='Auto-recovering...';document.getElementById('progressFill').style.width='50%';const r=await fetch(`?action=auto_recover_all&limit=${limit}`).then(r=>r.json());document.getElementById('progressFill').style.width='100%';document.getElementById('progressText').textContent=`Done! ${r.recovered}/${r.processed} recovered`;document.getElementById('progressCloseBtn').disabled=false;loadAll()}
|
|
async function activatePending(){if(!confirm('Activate all pending accounts?'))return;openModal('progressModal');document.getElementById('progressCloseBtn').disabled=true;document.getElementById('progressText').textContent='Activating...';const r=await fetch('?action=activate_pending').then(r=>r.json());document.getElementById('progressFill').style.width='100%';document.getElementById('progressText').textContent=`Done! ${r.activated} activated, ${r.failed} failed`;document.getElementById('progressCloseBtn').disabled=false;loadAll()}
|
|
function openModal(id){document.getElementById(id).classList.add('show')}
|
|
function closeModal(id){document.getElementById(id).classList.remove('show')}
|
|
function toast(msg,type='success'){const t=document.getElementById('toast');t.className='toast show '+type;document.getElementById('toastText').textContent=msg;setTimeout(()=>t.classList.remove('show'),4000)}
|
|
function loadAll(){loadStats();loadAccounts(currentPage);loadLogs()}
|
|
loadAll();setInterval(loadStats,30000);
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|