Spaces:
Sleeping
Sleeping
File size: 47,566 Bytes
f759742 b8f94df f759742 b8f94df f759742 b8f94df f759742 b8f94df f759742 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 | <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin Panel — AttendAI Pro</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="css/main.css">
<link rel="stylesheet" href="css/teacher.css">
<link rel="stylesheet" href="css/admin.css">
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<script src="https://accounts.google.com/gsi/client" async defer></script>
</head>
<body>
<!-- Admin Auth Wall (shown if not logged in as admin) -->
<div id="adminAuthWall" class="admin-auth-wall">
<div class="admin-auth-card">
<div class="admin-auth-header">
<div class="admin-shield">🛡️</div>
<h1>Developer / Admin Access</h1>
<p>Restricted area. Only authorized administrators can access this panel.</p>
</div>
<div id="adminAuthAlert" style="margin-bottom:16px;display:none"></div>
<div class="role-reminder admin-reminder">
<div class="reminder-icon">⚠️</div>
<div>
<strong>Restricted Access</strong>
<p>Your Google account must be registered with admin role. All access attempts are logged.</p>
</div>
</div>
<div style="margin:24px 0; display: flex; justify-content: center;">
<div id="adminGoogleBtnContainer"></div>
</div>
<div id="adminLoadingState" class="auth-loading hidden">
<div class="spinner spinner-dark"></div>
<span>Verifying admin credentials...</span>
</div>
<div class="auth-notice-box">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 8v4M12 16h.01"/></svg>
<span>All admin actions are logged with IP, timestamp, and user identity.</span>
</div>
<div style="text-align:center;margin-top:16px">
<a href="index.html" style="font-size:13px;color:var(--gray)">← Return to Home</a>
</div>
</div>
</div>
<!-- Admin Dashboard -->
<div id="adminDashboard" style="display:none">
<!-- Sidebar -->
<aside class="sidebar" id="sidebar">
<div class="sidebar-logo">
<span class="logo-icon">🛡️</span>
Admin<span>Panel</span>
</div>
<nav class="sidebar-nav">
<div class="nav-section-title">System</div>
<button class="nav-item active" onclick="showSection('dashboard', this)"><span class="nav-icon">📊</span> Dashboard</button>
<button class="nav-item" onclick="showSection('users', this)"><span class="nav-icon">👥</span> User Management</button>
<button class="nav-item" onclick="showSection('sessions', this)"><span class="nav-icon">📱</span> Sessions</button>
<button class="nav-item" onclick="showSection('attendance', this)"><span class="nav-icon">✅</span> Attendance</button>
<div class="nav-section-title">Security</div>
<button class="nav-item" onclick="showSection('fraud', this)">
<span class="nav-icon">🚨</span> Fraud Alerts
<span class="notif-badge" id="fraudBadge" style="position:relative;margin-left:auto;font-size:10px;background:var(--danger)">0</span>
</button>
<button class="nav-item" onclick="showSection('logs', this)"><span class="nav-icon">📋</span> Audit Logs</button>
<div class="nav-section-title">Configuration</div>
<button class="nav-item" onclick="showSection('config', this)"><span class="nav-icon">⚙️</span> System Config</button>
<button class="nav-item" onclick="showSection('analytics', this)"><span class="nav-icon">📈</span> Global Analytics</button>
</nav>
<div class="sidebar-footer">
<div class="sidebar-user">
<img id="adminSidebarAvatar" src="" alt="" class="avatar avatar-sm">
<div class="sidebar-user-info">
<div class="sidebar-user-name" id="adminSidebarName">—</div>
<div class="sidebar-user-role" style="color:#F59E0B">⚡ Administrator</div>
</div>
</div>
<button class="nav-item" onclick="logout()" style="color:rgba(239,68,68,0.8)"><span class="nav-icon">🚪</span> Sign Out</button>
</div>
</aside>
<div class="mobile-overlay" id="mobileOverlay" onclick="closeSidebar()"></div>
<main class="main-content">
<div class="topbar">
<div style="display:flex;align-items:center;gap:12px">
<button class="hamburger" onclick="toggleSidebar()">☰</button>
<div>
<div class="topbar-title" id="topbarTitle">System Dashboard</div>
<div class="text-xs text-gray" id="adminTopbarSub">AttendAI Pro — Administrator View</div>
</div>
</div>
<div class="topbar-actions">
<div class="admin-badge">⚡ ADMIN</div>
<button class="btn btn-sm btn-secondary" onclick="refreshAll()">🔄 Refresh</button>
</div>
</div>
<div class="page-content">
<!-- ===== DASHBOARD ===== -->
<div class="section active fade-in" id="section-dashboard">
<div class="stats-grid" id="systemStats">
<div class="stat-card blue"><div class="stat-icon">👥</div><div class="stat-value" id="sys-users">—</div><div class="stat-label">Total Users</div></div>
<div class="stat-card green"><div class="stat-icon">🎓</div><div class="stat-value" id="sys-students">—</div><div class="stat-label">Students</div></div>
<div class="stat-card purple"><div class="stat-icon">👨🏫</div><div class="stat-value" id="sys-teachers">—</div><div class="stat-label">Teachers</div></div>
<div class="stat-card yellow"><div class="stat-icon">📱</div><div class="stat-value" id="sys-sessions">—</div><div class="stat-label">Total Sessions</div></div>
<div class="stat-card green"><div class="stat-icon">✅</div><div class="stat-value" id="sys-att">—</div><div class="stat-label">Attendance Records</div></div>
<div class="stat-card red"><div class="stat-icon">🚨</div><div class="stat-value" id="sys-fraud">—</div><div class="stat-label">Fraud Alerts</div></div>
</div>
<!-- Live Sessions -->
<div class="card">
<div class="section-header"><div class="section-title">🔴 Live Sessions</div><span class="live-dot">LIVE</span></div>
<div id="liveSessionsList">
<div class="empty-state" style="padding:30px"><div class="empty-state-icon">📱</div><h3>No Active Sessions</h3><p>No teachers are currently running attendance sessions.</p></div>
</div>
</div>
<!-- Recent Logs -->
<div class="card">
<div class="section-header"><div class="section-title">📋 Recent Activity</div><a href="#" onclick="showSection('logs',document.querySelector('[onclick*=logs]'))" class="btn btn-sm btn-secondary">View All</a></div>
<div id="recentLogs">
<div class="empty-state" style="padding:30px"><div class="empty-state-icon">📋</div><p>No recent activity</p></div>
</div>
</div>
</div>
<!-- ===== USER MANAGEMENT ===== -->
<div class="section hidden" id="section-users">
<div class="section-header" style="margin-bottom:20px">
<div>
<h2>User Management</h2>
<p class="text-sm text-gray">Add, edit, and manage all system users. Only pre-registered users can log in.</p>
</div>
<button class="btn btn-primary" onclick="openAddUserModal()">+ Add User</button>
</div>
<!-- Tabs -->
<div class="user-tabs">
<button class="utab active" onclick="filterUsers('all', this)">All Users</button>
<button class="utab" onclick="filterUsers('student', this)">Students</button>
<button class="utab" onclick="filterUsers('teacher', this)">Teachers</button>
<button class="utab" onclick="filterUsers('admin', this)">Admins</button>
</div>
<!-- Search -->
<div style="margin:16px 0;display:flex;gap:12px">
<input type="text" class="form-control" id="userSearchInput" placeholder="Search by name, email, or roll number..." oninput="searchUsers()" style="flex:1">
<button class="btn btn-secondary" onclick="exportUsers()">⬇️ Export</button>
</div>
<div class="card p-0">
<div class="table-wrapper">
<table class="table">
<thead>
<tr><th>Name</th><th>Email</th><th>Role</th><th>Roll/Dept</th><th>Institution</th><th>Status</th><th>Last Login</th><th>Actions</th></tr>
</thead>
<tbody id="usersTableBody">
<tr><td colspan="8" style="text-align:center;padding:32px;color:var(--gray)">Loading users...</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- ===== SESSIONS ===== -->
<div class="section hidden" id="section-sessions">
<div class="section-header" style="margin-bottom:20px">
<h2>All Sessions</h2>
<button class="btn btn-sm btn-danger" id="forceEndBtn" onclick="forceEndAllSessions()" style="display:none">Force End All Live Sessions</button>
</div>
<div class="card p-0">
<div class="table-wrapper">
<table class="table">
<thead><tr><th>Subject</th><th>Class</th><th>Teacher</th><th>Started</th><th>Status</th><th>Students</th><th>Actions</th></tr></thead>
<tbody id="adminSessionsBody">
<tr><td colspan="7" style="text-align:center;padding:32px;color:var(--gray)">Loading sessions...</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- ===== ATTENDANCE ===== -->
<div class="section hidden" id="section-attendance">
<div class="section-header" style="margin-bottom:20px">
<div><h2>All Attendance Records</h2><p class="text-sm text-gray">Complete verified attendance database</p></div>
<button class="btn btn-success" onclick="exportAllAttendance()">⬇️ Export All</button>
</div>
<div style="display:flex;gap:12px;margin-bottom:16px;flex-wrap:wrap">
<select class="form-control form-select" id="adminAttFilter" onchange="loadAdminAttendance()" style="width:auto">
<option value="all">All Time</option>
<option value="today">Today</option>
<option value="week">This Week</option>
<option value="month">This Month</option>
</select>
<select class="form-control form-select" id="adminAttStatus" onchange="loadAdminAttendance()" style="width:auto">
<option value="">All Statuses</option>
<option value="present">Present</option>
<option value="absent">Absent</option>
<option value="late">Late</option>
<option value="fraudulent">Fraudulent</option>
</select>
</div>
<div class="card p-0">
<div class="table-wrapper">
<table class="table">
<thead><tr><th>Roll No.</th><th>Student</th><th>Session</th><th>Status</th><th>Time</th><th>GPS</th><th>Face</th><th>Fraud Flags</th></tr></thead>
<tbody id="adminAttBody">
<tr><td colspan="8" style="text-align:center;padding:32px;color:var(--gray)">Loading...</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- ===== FRAUD ALERTS ===== -->
<div class="section hidden" id="section-fraud">
<div class="section-header" style="margin-bottom:20px">
<h2>🚨 Fraud Detection Center</h2>
<button class="btn btn-sm btn-secondary" onclick="loadAdminFraud()">🔄 Refresh</button>
</div>
<div id="adminFraudList">
<div class="empty-state" style="padding:60px"><div class="empty-state-icon">🛡️</div><h3>No Alerts</h3><p>No fraud detected in the system.</p></div>
</div>
</div>
<!-- ===== AUDIT LOGS ===== -->
<div class="section hidden" id="section-logs">
<div class="section-header" style="margin-bottom:20px">
<h2>📋 Audit Logs</h2>
<div style="display:flex;gap:8px">
<select class="form-control form-select" id="logFilter" onchange="loadAuditLogs()" style="width:auto">
<option value="">All Actions</option>
<option value="LOGIN_SUCCESS">Logins</option>
<option value="LOGIN_FAILED">Failed Logins</option>
<option value="ATTENDANCE_MARKED">Attendance</option>
<option value="SESSION_CREATED">Sessions</option>
<option value="FRAUD_ATTEMPT">Fraud Attempts</option>
</select>
</div>
</div>
<div class="card p-0">
<div class="table-wrapper">
<table class="table">
<thead><tr><th>Timestamp</th><th>User</th><th>Action</th><th>Resource</th><th>Status</th></tr></thead>
<tbody id="logsTableBody">
<tr><td colspan="5" style="text-align:center;padding:32px;color:var(--gray)">Loading logs...</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- ===== SYSTEM CONFIG ===== -->
<div class="section hidden" id="section-config">
<h2 style="margin-bottom:24px">⚙️ System Configuration</h2>
<div class="grid-2">
<div class="card">
<h3 style="margin-bottom:20px">Attendance Rules</h3>
<div class="form-group">
<label class="form-label">QR Rotation Interval (seconds)</label>
<input type="number" class="form-control" id="cfg-qr" value="60" min="10" max="300">
</div>
<div class="form-group">
<label class="form-label">Default Geofence Radius (meters)</label>
<input type="number" class="form-control" id="cfg-geo" value="100" min="10" max="5000">
</div>
<div class="form-group">
<label class="form-label">Attendance Warning Threshold (%)</label>
<input type="number" class="form-control" id="cfg-threshold" value="75" min="1" max="100">
</div>
<div class="form-group">
<label class="form-label">Face Match Confidence (0-1)</label>
<input type="number" class="form-control" id="cfg-face" value="0.6" min="0.1" max="1" step="0.05">
</div>
<button class="btn btn-primary" onclick="saveConfig()">💾 Save Settings</button>
</div>
<div class="card">
<h3 style="margin-bottom:20px">Institution Settings</h3>
<div class="form-group">
<label class="form-label">Institution Name</label>
<input type="text" class="form-control" id="cfg-institution" placeholder="University Name">
</div>
<div class="form-group">
<label class="form-label">Google Client ID</label>
<input type="text" class="form-control" id="cfg-oauth" placeholder="xxx.apps.googleusercontent.com">
<div class="form-hint">Required for Google OAuth login</div>
</div>
<div class="form-group">
<label class="form-label">Allowed Student Email Domains</label>
<input type="text" class="form-control" id="cfg-domains" placeholder="university.edu, college.ac.in (comma separated)">
<div class="form-hint">Leave blank to allow all domains</div>
</div>
<div class="form-group">
<label class="form-label">Max Attendance Attempts per Session</label>
<input type="number" class="form-control" id="cfg-attempts" value="1" min="1" max="5">
</div>
<button class="btn btn-primary" onclick="saveConfig()">💾 Save Settings</button>
</div>
</div>
<!-- Danger Zone -->
<div class="card" style="margin-top:20px;border-color:rgba(239,68,68,0.3)">
<h3 style="color:var(--danger);margin-bottom:16px">⚠️ Danger Zone</h3>
<div style="display:flex;gap:12px;flex-wrap:wrap">
<button class="btn btn-danger" onclick="confirmAction('clear_fraud', 'Mark all fraud alerts as resolved?')">Clear All Fraud Alerts</button>
<button class="btn btn-danger" onclick="confirmAction('end_sessions', 'Force end ALL active sessions?')">Force End All Sessions</button>
</div>
<p class="text-sm text-gray" style="margin-top:12px">These actions are irreversible and will be logged.</p>
</div>
</div>
<!-- ===== ANALYTICS ===== -->
<div class="section hidden" id="section-analytics">
<h2 style="margin-bottom:24px">📈 Global System Analytics</h2>
<div class="grid-2">
<div class="card"><div class="section-title" style="margin-bottom:16px">Daily Attendance Trend</div><div style="height:280px"><canvas id="adminTrendChart"></canvas></div></div>
<div class="card"><div class="section-title" style="margin-bottom:16px">User Distribution</div><div style="height:280px"><canvas id="adminUserChart"></canvas></div></div>
</div>
<div class="card" style="margin-top:20px">
<div class="section-title" style="margin-bottom:16px">Fraud Detection Summary</div>
<div style="height:250px"><canvas id="adminFraudChart"></canvas></div>
</div>
</div>
</div>
</main>
</div><!-- /adminDashboard -->
<!-- Add User Modal -->
<div class="modal-overlay" id="addUserModal">
<div class="modal" style="max-width:540px">
<div class="modal-header">
<h3>➕ Add New User</h3>
<button class="modal-close" onclick="closeModal('addUserModal')">×</button>
</div>
<div class="modal-body">
<div id="addUserAlert" style="margin-bottom:12px;display:none"></div>
<div class="form-group">
<label class="form-label">Full Name *</label>
<input type="text" class="form-control" id="nu-name" placeholder="John Doe">
</div>
<div class="form-group">
<label class="form-label">Google Email *</label>
<input type="email" class="form-control" id="nu-email" placeholder="student@university.edu">
<div class="form-hint">Must match their Google account email exactly</div>
</div>
<div class="form-group">
<label class="form-label">Role *</label>
<select class="form-control form-select" id="nu-role" onchange="toggleRoleFields()">
<option value="student">Student</option>
<option value="teacher">Teacher</option>
<option value="admin">Administrator</option>
</select>
</div>
<div id="studentFields">
<div class="form-group">
<label class="form-label">Roll Number *</label>
<input type="text" class="form-control" id="nu-roll" placeholder="CS2024001">
</div>
</div>
<div class="form-group">
<label class="form-label">Department</label>
<input type="text" class="form-control" id="nu-dept" placeholder="Computer Science">
</div>
<div class="form-group">
<label class="form-label">Institution</label>
<input type="text" class="form-control" id="nu-institution" placeholder="University Name">
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="closeModal('addUserModal')">Cancel</button>
<button class="btn btn-primary" onclick="addUser()">Add User</button>
</div>
</div>
</div>
<div class="toast-container" id="toastContainer"></div>
<script src="js/config.js"></script>
<script>
let currentAdmin = null;
let allUsers = [];
let currentUserFilter = 'all';
let adminTrendChart = null, adminUserChart = null, adminFraudChart = null;
/* ===== Auth ===== */
async function checkAdminAuth() {
const user = AuthState.get();
if (user && user.role === 'admin') {
currentAdmin = user;
showAdminDashboard();
}
// If not logged in, show auth wall
}
function showAdminDashboard() {
document.getElementById('adminAuthWall').style.display = 'none';
document.getElementById('adminDashboard').style.display = 'block';
document.getElementById('adminSidebarAvatar').src = currentAdmin.picture || `https://ui-avatars.com/api/?name=${encodeURIComponent(currentAdmin.name)}&background=F59E0B&color=fff`;
document.getElementById('adminSidebarName').textContent = currentAdmin.name;
loadDashboard();
loadAllUsers();
loadAdminSessions();
loadAdminAttendance();
loadAdminFraud();
loadAuditLogs();
}
function handleGoogleCredential(response) {
try {
const parts = response.credential.split('.');
const payload = JSON.parse(atob(parts[1].replace(/-/g, '+').replace(/_/g, '/')));
verifyAdminInDB({ sub: payload.sub, email: payload.email, name: payload.name, picture: payload.picture });
} catch(e) {
showAdminAuthAlert('Authentication failed. Please try again.', 'danger');
}
}
async function verifyAdminInDB(googleUser) {
document.getElementById('adminLoadingState').classList.remove('hidden');
document.getElementById('adminGoogleBtn').disabled = true;
try {
// Exact email lookup
const result = await API.get('users', { email: googleUser.email });
const users = result.data || [];
const user = users.find(u => u.email && u.email.toLowerCase() === googleUser.email.toLowerCase());
if (!user) {
showAdminAuthAlert('Your email is not registered in the system.', 'danger');
return;
}
if (user.role !== 'admin') {
showAdminAuthAlert(`Your account role is "${user.role}", not admin. Please use the correct portal.`, 'danger');
await Logger.log('ADMIN_ACCESS_DENIED', 'auth', { email: googleUser.email, role: user.role }, 'failure');
return;
}
if (user.is_active === false) {
showAdminAuthAlert('Your admin account is deactivated.', 'danger');
return;
}
currentAdmin = { ...user, name: googleUser.name || user.name, picture: googleUser.picture || '' };
AuthState.save(currentAdmin);
await API.patch('users', user.id, { last_login: Date.now() });
await Logger.log('ADMIN_LOGIN', 'auth', { email: googleUser.email }, 'success');
showAdminDashboard();
} catch(e) {
showAdminAuthAlert('System error during verification. Please try again.', 'danger');
} finally {
document.getElementById('adminLoadingState').classList.add('hidden');
// Official button is managed by Google
}
}
function showAdminAuthAlert(msg, type) {
const el = document.getElementById('adminAuthAlert');
el.style.display = 'block';
el.innerHTML = `<div class="alert alert-${type}"><span>${type==='danger'?'❌':'⚠️'}</span><div>${msg}</div></div>`;
}
function initiateAdminLogin() {
if (APP_CONFIG.googleClientId === 'YOUR_GOOGLE_CLIENT_ID.apps.googleusercontent.com') {
showAdminAuthAlert('<strong>Google OAuth Not Configured</strong><br>Set up Google OAuth Client ID in js/config.js to enable login.', 'warning');
return;
}
if (typeof google !== 'undefined' && google.accounts) google.accounts.id.prompt();
else showAdminAuthAlert('Google auth unavailable.', 'warning');
}
/* ===== Navigation ===== */
function showSection(name, el) {
document.querySelectorAll('.section').forEach(s => { s.classList.remove('active'); s.classList.add('hidden'); });
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
const sec = document.getElementById('section-' + name);
if (sec) { sec.classList.remove('hidden'); sec.classList.add('active', 'fade-in'); }
if (el) el.classList.add('active');
const titles = { dashboard:'System Dashboard', users:'User Management', sessions:'All Sessions', attendance:'Attendance Records', fraud:'Fraud Detection', logs:'Audit Logs', config:'System Config', analytics:'Global Analytics' };
document.getElementById('topbarTitle').textContent = titles[name] || name;
if (name === 'analytics') loadAnalytics();
closeSidebar();
}
function toggleSidebar() { document.getElementById('sidebar').classList.toggle('open'); document.getElementById('mobileOverlay').classList.toggle('open'); }
function closeSidebar() { document.getElementById('sidebar').classList.remove('open'); document.getElementById('mobileOverlay').classList.remove('open'); }
/* ===== Dashboard ===== */
async function loadDashboard() {
try {
const [uRes, sRes, aRes, fRes] = await Promise.all([
API.get('users', { limit: 500 }),
API.get('sessions', { limit: 200 }),
API.get('attendance', { limit: 1000 }),
API.get('fraud_alerts', { limit: 200 })
]);
const users = uRes.data || [];
const sessions = sRes.data || [];
const att = aRes.data || [];
const fraud = fRes.data || [];
document.getElementById('sys-users').textContent = users.length;
document.getElementById('sys-students').textContent = users.filter(u => u.role === 'student').length;
document.getElementById('sys-teachers').textContent = users.filter(u => u.role === 'teacher').length;
document.getElementById('sys-sessions').textContent = sessions.length;
document.getElementById('sys-att').textContent = att.length;
document.getElementById('sys-fraud').textContent = fraud.filter(f => !f.resolved).length;
document.getElementById('fraudBadge').textContent = fraud.filter(f => !f.resolved).length;
// Live sessions
const liveSessions = sessions.filter(s => s.is_active);
document.getElementById('forceEndBtn').style.display = liveSessions.length ? 'block' : 'none';
const liveEl = document.getElementById('liveSessionsList');
if (!liveSessions.length) {
liveEl.innerHTML = `<div class="empty-state" style="padding:30px"><div class="empty-state-icon">📱</div><h3>No Active Sessions</h3><p>No teachers are currently running attendance sessions.</p></div>`;
} else {
const userMap = {};
users.forEach(u => { userMap[u.id] = u; });
liveEl.innerHTML = liveSessions.map(s => {
const teacher = userMap[s.teacher_id] || {};
return `<div class="live-session-row"><div class="live-dot">LIVE</div><div style="flex:1"><strong>${s.subject}</strong> — ${s.class_name}</div><div class="text-sm text-gray">by ${teacher.name||'Unknown'}</div><div class="text-xs text-gray">${Utils.timeAgo(s.started_at||s.created_at)}</div></div>`;
}).join('');
}
} catch(e) { console.error('Dashboard load error:', e); }
}
/* ===== User Management ===== */
async function loadAllUsers() {
try {
const result = await API.get('users', { limit: 500 });
allUsers = result.data || [];
renderUsersTable(allUsers);
} catch(e) {}
}
function filterUsers(role, el) {
document.querySelectorAll('.utab').forEach(t => t.classList.remove('active'));
if (el) el.classList.add('active');
currentUserFilter = role;
const filtered = role === 'all' ? allUsers : allUsers.filter(u => u.role === role);
renderUsersTable(filtered);
}
function searchUsers() {
const q = document.getElementById('userSearchInput').value.toLowerCase();
let filtered = currentUserFilter === 'all' ? allUsers : allUsers.filter(u => u.role === currentUserFilter);
if (q) filtered = filtered.filter(u => (u.name||'').toLowerCase().includes(q) || (u.email||'').toLowerCase().includes(q) || (u.roll_number||'').toLowerCase().includes(q));
renderUsersTable(filtered);
}
function renderUsersTable(users) {
const tbody = document.getElementById('usersTableBody');
if (!users.length) {
tbody.innerHTML = `<tr><td colspan="8" style="text-align:center;padding:40px;color:var(--gray)"><div class="empty-state-icon">👥</div><p>No users found. Add users to get started.</p></td></tr>`;
return;
}
const roleColors = { student: 'info', teacher: 'primary', admin: 'warning' };
tbody.innerHTML = users.map(u => `<tr>
<td><div style="display:flex;align-items:center;gap:10px"><img src="${u.picture||`https://ui-avatars.com/api/?name=${encodeURIComponent(u.name||'?')}&background=4F46E5&color=fff`}" class="avatar avatar-sm">${u.name||'—'}</div></td>
<td class="text-sm text-gray">${u.email||'—'}</td>
<td><span class="badge badge-${roleColors[u.role]||'gray'}">${u.role||'—'}</span></td>
<td class="text-sm">${u.roll_number||u.department||'—'}</td>
<td class="text-sm">${u.institution||'—'}</td>
<td><span class="badge badge-${u.is_active!==false?'success':'danger'}">${u.is_active!==false?'Active':'Inactive'}</span></td>
<td class="text-sm text-gray">${u.last_login ? Utils.timeAgo(u.last_login) : 'Never'}</td>
<td>
<div style="display:flex;gap:6px">
<button class="btn btn-sm btn-secondary" onclick="toggleUserStatus('${u.id}', ${u.is_active!==false})">${u.is_active!==false?'Disable':'Enable'}</button>
<button class="btn btn-sm btn-danger" onclick="deleteUser('${u.id}', '${u.name}')">Delete</button>
</div>
</td>
</tr>`).join('');
}
async function toggleUserStatus(id, isActive) {
if (!confirm(`${isActive ? 'Disable' : 'Enable'} this user? ${isActive ? 'They will not be able to log in.' : ''}`)) return;
try {
await API.patch('users', id, { is_active: !isActive });
await Logger.log(isActive ? 'USER_DISABLED' : 'USER_ENABLED', 'users', { user_id: id });
Toast.success(`User ${isActive ? 'disabled' : 'enabled'}`);
await loadAllUsers();
filterUsers(currentUserFilter, null);
} catch(e) { Toast.error('Error updating user'); }
}
async function deleteUser(id, name) {
if (!confirm(`Permanently delete user "${name}"? This cannot be undone.`)) return;
try {
await API.delete('users', id);
await Logger.log('USER_DELETED', 'users', { user_id: id, name });
Toast.success('User deleted');
await loadAllUsers();
filterUsers(currentUserFilter, null);
} catch(e) { Toast.error('Error deleting user'); }
}
function openAddUserModal() { openModal('addUserModal'); }
function toggleRoleFields() {
const role = document.getElementById('nu-role').value;
document.getElementById('studentFields').style.display = role === 'student' ? 'block' : 'none';
}
async function addUser() {
const name = document.getElementById('nu-name').value.trim();
const email = document.getElementById('nu-email').value.trim().toLowerCase();
const role = document.getElementById('nu-role').value;
const roll = document.getElementById('nu-roll').value.trim().toUpperCase();
const dept = document.getElementById('nu-dept').value.trim();
const institution = document.getElementById('nu-institution').value.trim();
if (!name || !email || !role) {
showModalAlert('Please fill in all required fields.', 'danger', 'addUserAlert');
return;
}
if (role === 'student' && !roll) {
showModalAlert('Roll number is required for students.', 'danger', 'addUserAlert');
return;
}
// Check for duplicate email
const existing = allUsers.find(u => u.email && u.email.toLowerCase() === email);
if (existing) {
showModalAlert('A user with this email already exists.', 'danger', 'addUserAlert');
return;
}
try {
const newUser = {
id: Utils.generateUUID(),
email,
name,
role,
roll_number: role === 'student' ? roll : null,
department: dept || null,
institution: institution || null,
is_active: true,
face_data: null,
picture: null,
last_login: null
};
await API.post('users', newUser);
await Logger.log('USER_ADDED', 'users', { email, role, name });
Toast.success(`User "${name}" added as ${role}`);
closeModal('addUserModal');
document.getElementById('nu-name').value = '';
document.getElementById('nu-email').value = '';
document.getElementById('nu-roll').value = '';
document.getElementById('nu-dept').value = '';
document.getElementById('nu-institution').value = '';
await loadAllUsers();
filterUsers(currentUserFilter, null);
} catch(e) { showModalAlert('Error adding user. Please try again.', 'danger', 'addUserAlert'); }
}
/* ===== Sessions Admin ===== */
async function loadAdminSessions() {
try {
const [sRes, uRes] = await Promise.all([API.get('sessions', { limit: 200 }), API.get('users', { limit: 200 })]);
const sessions = (sRes.data||[]).sort((a,b) => (b.started_at||b.created_at)-(a.started_at||a.created_at));
const userMap = {};
(uRes.data||[]).forEach(u => { userMap[u.id] = u; });
const tbody = document.getElementById('adminSessionsBody');
if (!sessions.length) {
tbody.innerHTML = `<tr><td colspan="7" style="text-align:center;padding:32px;color:var(--gray)"><p>No sessions yet</p></td></tr>`;
return;
}
tbody.innerHTML = sessions.map(s => {
const teacher = userMap[s.teacher_id] || {};
return `<tr>
<td><strong>${s.subject}</strong></td>
<td>${s.class_name}</td>
<td>${teacher.name||'Unknown'}</td>
<td class="text-sm">${Utils.formatDateTime(s.started_at||s.created_at)}</td>
<td><span class="badge badge-${s.is_active?'danger':'gray'}">${s.is_active?'🔴 LIVE':'Ended'}</span></td>
<td>${s.total_students||'—'}</td>
<td>${s.is_active ? `<button class="btn btn-sm btn-danger" onclick="forceEndSession('${s.id}')">Force End</button>` : '—'}</td>
</tr>`;
}).join('');
} catch(e) {}
}
async function forceEndSession(id) {
if (!confirm('Force end this session?')) return;
try {
await API.patch('sessions', id, { is_active: false, ended_at: Date.now() });
await Logger.log('SESSION_FORCE_ENDED', 'sessions', { session_id: id });
Toast.success('Session force-ended');
await loadAdminSessions();
} catch(e) { Toast.error('Error ending session'); }
}
async function forceEndAllSessions() {
if (!confirm('Force end ALL active sessions? This will stop all ongoing attendance marking.')) return;
try {
const result = await API.get('sessions', { limit: 100 });
const live = (result.data||[]).filter(s => s.is_active);
await Promise.all(live.map(s => API.patch('sessions', s.id, { is_active: false, ended_at: Date.now() })));
Toast.success(`${live.length} sessions ended`);
await loadAdminSessions();
await loadDashboard();
} catch(e) { Toast.error('Error ending sessions'); }
}
/* ===== Attendance Admin ===== */
async function loadAdminAttendance() {
try {
const [aRes, uRes, sRes] = await Promise.all([
API.get('attendance', { limit: 1000 }),
API.get('users', { limit: 500 }),
API.get('sessions', { limit: 200 })
]);
const userMap = {};
(uRes.data||[]).forEach(u => { userMap[u.id] = u; });
const sessMap = {};
(sRes.data||[]).forEach(s => { sessMap[s.id] = s; });
let records = aRes.data || [];
const rangeFilter = document.getElementById('adminAttFilter')?.value;
const statusFilter = document.getElementById('adminAttStatus')?.value;
const ranges = { today: Date.now()-86400000, week: Date.now()-7*86400000, month: Date.now()-30*86400000, all: 0 };
const cutoff = ranges[rangeFilter] || 0;
records = records.filter(r => (r.marked_at||r.created_at) >= cutoff);
if (statusFilter) records = records.filter(r => r.status === statusFilter);
records.sort((a,b) => (b.marked_at||b.created_at) - (a.marked_at||a.created_at));
const tbody = document.getElementById('adminAttBody');
if (!records.length) {
tbody.innerHTML = `<tr><td colspan="8" style="text-align:center;padding:32px;color:var(--gray)"><div class="empty-state-icon">📋</div><p>No records for selected filters</p></td></tr>`;
return;
}
tbody.innerHTML = records.map(r => {
const u = userMap[r.student_id] || {};
const s = sessMap[r.session_id] || {};
const flags = r.fraud_flags && r.fraud_flags.length ? `<span class="badge badge-danger">${r.fraud_flags.join(',')}</span>` : '—';
return `<tr>
<td><strong>${r.roll_number||'—'}</strong></td>
<td class="text-sm">${u.name||'Unknown'}</td>
<td class="text-sm">${s.subject||'—'}</td>
<td><span class="badge badge-${r.status==='present'||r.status==='late'?'success':r.status==='fraudulent'?'primary':'danger'}">${r.status}</span></td>
<td class="text-sm">${Utils.formatDateTime(r.marked_at||r.created_at)}</td>
<td class="text-sm text-gray">${r.gps_lat?`${r.gps_lat.toFixed(4)},${r.gps_lng.toFixed(4)}`:'—'}</td>
<td class="text-sm">${r.face_match_score!=null?`${(r.face_match_score*100).toFixed(0)}%`:'—'}</td>
<td>${flags}</td>
</tr>`;
}).join('');
} catch(e) { console.error('Attendance admin error:', e); }
}
/* ===== Fraud Admin ===== */
async function loadAdminFraud() {
try {
const [fRes, uRes] = await Promise.all([API.get('fraud_alerts', { limit: 500 }), API.get('users', { limit: 200 })]);
const alerts = (fRes.data||[]).sort((a,b) => (b.detected_at||b.created_at)-(a.detected_at||a.created_at));
const userMap = {};
(uRes.data||[]).forEach(u => { userMap[u.id] = u; });
document.getElementById('fraudBadge').textContent = alerts.filter(a => !a.resolved).length;
const el = document.getElementById('adminFraudList');
if (!alerts.length) {
el.innerHTML = `<div class="empty-state" style="padding:60px"><div class="empty-state-icon">🛡️</div><h3>No Fraud Alerts</h3><p>System is clean. No suspicious activity detected.</p></div>`;
return;
}
el.innerHTML = alerts.map(a => {
const s = userMap[a.student_id] || {};
return `<div class="fraud-alert-card" style="margin-bottom:12px">
<div class="fraud-alert-icon">🚨</div>
<div class="fraud-alert-content" style="flex:1">
<h4>${a.alert_type||'Suspicious Activity'}</h4>
<p>${a.description}</p>
<div style="display:flex;gap:8px;align-items:center;margin-top:6px;flex-wrap:wrap">
<span class="badge badge-${a.severity==='high'?'danger':a.severity==='medium'?'warning':'info'}">${a.severity}</span>
${s.name ? `<span class="text-sm">Student: <strong>${s.name}</strong> (${s.roll_number||'—'})</span>` : ''}
<span class="text-xs text-gray">${Utils.formatDateTime(a.detected_at||a.created_at)}</span>
${a.resolved ? '<span class="badge badge-success">Resolved</span>' : ''}
</div>
</div>
${!a.resolved ? `<button class="btn btn-sm btn-secondary" onclick="resolveAlert('${a.id}')">Resolve</button>` : ''}
</div>`;
}).join('');
} catch(e) {}
}
async function resolveAlert(id) {
try {
await API.patch('fraud_alerts', id, { resolved: true });
Toast.success('Alert resolved');
await loadAdminFraud();
} catch(e) { Toast.error('Error resolving alert'); }
}
/* ===== Audit Logs ===== */
async function loadAuditLogs() {
try {
const result = await API.get('system_logs', { limit: 200 });
let logs = (result.data||[]).sort((a,b) => (b.timestamp||b.created_at)-(a.timestamp||a.created_at));
const filter = document.getElementById('logFilter')?.value;
if (filter) logs = logs.filter(l => l.action === filter);
const tbody = document.getElementById('logsTableBody');
if (!logs.length) {
tbody.innerHTML = `<tr><td colspan="5" style="text-align:center;padding:32px;color:var(--gray)">No logs found</td></tr>`;
return;
}
const uRes = await API.get('users', { limit: 200 });
const userMap = {};
(uRes.data||[]).forEach(u => { userMap[u.id] = u; });
tbody.innerHTML = logs.map(l => {
const user = userMap[l.user_id] || {};
return `<tr>
<td class="text-sm">${Utils.formatDateTime(l.timestamp||l.created_at)}</td>
<td class="text-sm">${user.name||l.user_id||'system'}</td>
<td><code style="font-size:12px;background:#F1F5F9;padding:2px 6px;border-radius:4px">${l.action||'—'}</code></td>
<td class="text-sm text-gray">${l.resource||'—'}</td>
<td><span class="badge badge-${l.status==='success'?'success':'danger'}">${l.status||'—'}</span></td>
</tr>`;
}).join('');
} catch(e) {}
}
/* ===== Analytics ===== */
async function loadAnalytics() {
try {
const [uRes, aRes, fRes] = await Promise.all([
API.get('users', { limit: 500 }),
API.get('attendance', { limit: 1000 }),
API.get('fraud_alerts', { limit: 200 })
]);
const users = uRes.data || [];
const att = aRes.data || [];
const fraudAlerts = fRes.data || [];
// Daily trend
const days = [];
for (let i = 6; i >= 0; i--) {
const d = new Date(Date.now() - i * 86400000);
const start = new Date(d.setHours(0,0,0,0)).getTime();
const dayAtt = att.filter(a => { const ts = a.marked_at||a.created_at; return ts >= start && ts < start+86400000; });
days.push({ label: d.toLocaleDateString('en-US',{weekday:'short'}), present: dayAtt.filter(a=>a.status==='present'||a.status==='late').length, total: dayAtt.length });
}
const tCtx = document.getElementById('adminTrendChart').getContext('2d');
if (adminTrendChart) adminTrendChart.destroy();
adminTrendChart = new Chart(tCtx, {
type: 'line',
data: { labels: days.map(d=>d.label), datasets: [
{ label: 'Present', data: days.map(d=>d.present), borderColor: '#10B981', backgroundColor: 'rgba(16,185,129,0.1)', fill: true, tension: 0.4 },
{ label: 'Total', data: days.map(d=>d.total), borderColor: '#4F46E5', backgroundColor: 'rgba(79,70,229,0.1)', fill: true, tension: 0.4 }
]},
options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom' } } }
});
// User distribution
const students = users.filter(u=>u.role==='student').length;
const teachers = users.filter(u=>u.role==='teacher').length;
const admins = users.filter(u=>u.role==='admin').length;
const uCtx = document.getElementById('adminUserChart').getContext('2d');
if (adminUserChart) adminUserChart.destroy();
adminUserChart = new Chart(uCtx, {
type: 'doughnut',
data: { labels: ['Students','Teachers','Admins'], datasets: [{ data: [students,teachers,admins], backgroundColor: ['#4F46E5','#06B6D4','#F59E0B'], borderWidth: 0 }]},
options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom' } } }
});
// Fraud summary
const fraudByType = {};
fraudAlerts.forEach(a => { fraudByType[a.alert_type||'Unknown'] = (fraudByType[a.alert_type||'Unknown']||0) + 1; });
const fCtx = document.getElementById('adminFraudChart').getContext('2d');
if (adminFraudChart) adminFraudChart.destroy();
adminFraudChart = new Chart(fCtx, {
type: 'bar',
data: { labels: Object.keys(fraudByType), datasets: [{ label: 'Alerts', data: Object.values(fraudByType), backgroundColor: 'rgba(239,68,68,0.7)', borderRadius: 6 }]},
options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { x: { grid: { display: false } }, y: { grid: { color: '#f1f5f9' }, ticks: { stepSize: 1 } } } }
});
} catch(e) { console.error('Analytics error:', e); }
}
/* ===== Config ===== */
async function saveConfig() {
Toast.success('Configuration saved successfully');
await Logger.log('CONFIG_UPDATED', 'system', { admin: currentAdmin.id });
}
/* ===== Recent Logs ===== */
async function loadRecentLogs() {
try {
const result = await API.get('system_logs', { limit: 10 });
const logs = (result.data||[]).sort((a,b) => (b.timestamp||b.created_at)-(a.timestamp||a.created_at)).slice(0,5);
const el = document.getElementById('recentLogs');
if (!logs.length) {
el.innerHTML = `<div class="empty-state" style="padding:20px"><p>No recent activity</p></div>`;
return;
}
const uRes = await API.get('users', { limit: 200 });
const userMap = {};
(uRes.data||[]).forEach(u => { userMap[u.id] = u; });
el.innerHTML = logs.map(l => {
const u = userMap[l.user_id] || {};
return `<div class="activity-item"><div class="activity-content"><code style="font-size:12px;background:#F1F5F9;padding:1px 6px;border-radius:4px">${l.action}</code> by <strong>${u.name||l.user_id||'system'}</strong></div><div class="activity-time">${Utils.timeAgo(l.timestamp||l.created_at)}</div></div>`;
}).join('');
} catch(e) {}
}
/* ===== Export ===== */
async function exportUsers() {
const headers = ['Name','Email','Role','Roll Number','Department','Institution','Status'];
const rows = allUsers.map(u => [u.name,u.email,u.role,u.roll_number||'',u.department||'',u.institution||'',u.is_active!==false?'Active':'Inactive'].map(v=>`"${v||''}"`).join(','));
const csv = [headers.join(','),...rows].join('\n');
const blob = new Blob([csv], { type: 'text/csv' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = `users_export_${Date.now()}.csv`;
a.click();
Toast.success('Users exported');
}
async function exportAllAttendance() {
Toast.info('Preparing export...');
const [aRes, uRes, sRes] = await Promise.all([API.get('attendance',{limit:2000}), API.get('users',{limit:500}), API.get('sessions',{limit:200})]);
const userMap = {};
(uRes.data||[]).forEach(u => { userMap[u.id] = u; });
const sessMap = {};
(sRes.data||[]).forEach(s => { sessMap[s.id] = s; });
const headers = ['Roll No','Name','Email','Status','Session','Time','GPS','Face Score','Fraud Flags'];
const rows = (aRes.data||[]).map(r => {
const u = userMap[r.student_id]||{};
const s = sessMap[r.session_id]||{};
return [r.roll_number||'',u.name||'',u.email||'',r.status,s.subject||'',Utils.formatDateTime(r.marked_at||r.created_at),r.gps_lat?`${r.gps_lat},${r.gps_lng}`:'',r.face_match_score||'',(r.fraud_flags||[]).join(';')].map(v=>`"${v||''}"`).join(',');
});
const csv = [headers.join(','),...rows].join('\n');
const blob = new Blob([csv], { type: 'text/csv' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = `all_attendance_${Date.now()}.csv`;
a.click();
Toast.success('Attendance exported');
}
/* ===== Modal Helpers ===== */
function openModal(id) { document.getElementById(id).classList.add('open'); }
function closeModal(id) { document.getElementById(id).classList.remove('open'); document.getElementById('addUserAlert').style.display='none'; }
function showModalAlert(msg, type, id) {
const el = document.getElementById(id);
el.style.display = 'block';
el.innerHTML = `<div class="alert alert-${type}"><span>${type==='danger'?'❌':'⚠️'}</span><div>${msg}</div></div>`;
}
async function confirmAction(action, msg) {
if (!confirm(msg)) return;
if (action === 'clear_fraud') {
const result = await API.get('fraud_alerts', { limit: 500 });
await Promise.all((result.data||[]).filter(a=>!a.resolved).map(a => API.patch('fraud_alerts', a.id, { resolved: true })));
Toast.success('All fraud alerts resolved');
await loadAdminFraud();
} else if (action === 'end_sessions') {
await forceEndAllSessions();
}
}
async function refreshAll() {
Toast.info('Refreshing...');
await Promise.all([loadDashboard(), loadAllUsers(), loadAdminSessions(), loadAdminFraud(), loadAuditLogs()]);
Toast.success('Refreshed');
}
/* ===== Init ===== */
window.addEventListener('load', () => {
checkAdminAuth();
if (APP_CONFIG.googleClientId !== 'YOUR_GOOGLE_CLIENT_ID.apps.googleusercontent.com' && typeof google !== 'undefined') {
google.accounts.id.initialize({ client_id: APP_CONFIG.googleClientId, callback: handleGoogleCredential, auto_select: false });
// Render official Google button
google.accounts.id.renderButton(
document.getElementById("adminGoogleBtnContainer"),
{ theme: "outline", size: "large", width: "100%", text: "signin_with", shape: "rectangular" }
);
}
});
</script>
</body>
</html>
|