| {% extends "base.html" %} |
| {% block title %}Admin — IrisAI{% endblock %} |
|
|
| {% block head %} |
| <style> |
| |
| .stats-grid { |
| display: grid; |
| grid-template-columns: repeat(4, 1fr); |
| gap: 1rem; |
| margin-bottom: 2rem; |
| } |
| @media (max-width: 700px) { .stats-grid { grid-template-columns: 1fr 1fr; } } |
| |
| .stat-card { |
| background: var(--surface); |
| border: 1px solid var(--border); |
| border-radius: var(--radius-lg); |
| padding: 1.25rem 1.5rem; |
| text-align: center; |
| } |
| .stat-card .val { |
| font-family: 'DM Serif Display', serif; |
| font-size: 2.8rem; |
| background: linear-gradient(135deg, var(--accent), var(--accent2)); |
| -webkit-background-clip: text; |
| -webkit-text-fill-color: transparent; |
| } |
| .stat-card .lbl { |
| font-size: .80rem; |
| text-transform: uppercase; |
| letter-spacing: .05em; |
| color: var(--muted); |
| margin-top: .2rem; |
| } |
| .stat-card.active-card { border-color: #4ade80; } |
| .stat-card.active-card .val { background: linear-gradient(135deg, #4ade80, #22d3ee); -webkit-background-clip: text; -webkit-text-fill-color: transparent; } |
| |
| |
| .section-title { |
| font-size: .80rem; |
| text-transform: uppercase; |
| letter-spacing: .08em; |
| color: var(--muted); |
| margin-bottom: .75rem; |
| font-weight: 700; |
| } |
| |
| |
| .dot { |
| display: inline-block; |
| width: 8px; height: 8px; |
| border-radius: 50%; |
| margin-right: 6px; |
| vertical-align: middle; |
| } |
| .dot-green { background: #4ade80; box-shadow: 0 0 6px #4ade8088; animation: pulse 1.5s infinite; } |
| .dot-gray { background: var(--muted); } |
| @keyframes pulse { |
| 0%, 100% { opacity: 1; } |
| 50% { opacity: .4; } |
| } |
| |
| |
| .refresh-badge { |
| font-family: 'DM Mono', monospace; |
| font-size: .80rem; |
| background: var(--surface2); |
| border: 1px solid var(--border); |
| color: var(--muted); |
| padding: .2rem .7rem; |
| border-radius: 20px; |
| } |
| #countdown { color: var(--accent); } |
| |
| |
| .admin-table th { white-space: nowrap; } |
| .admin-table td { font-size: .82rem; white-space: nowrap; } |
| .overflow-x { overflow-x: auto; } |
| input[type="email"] { |
| width: 100%; padding: .75rem 1rem; |
| background: var(--surface2); border: 1px solid var(--border); |
| border-radius: var(--radius); color: var(--text); |
| font-family: inherit; font-size: .95rem; outline: none; |
| transition: border-color .2s, box-shadow .2s; |
| } |
| input[type="email"]:focus { |
| border-color: var(--accent); |
| box-shadow: 0 0 0 3px rgba(124,111,255,.15); |
| } |
| </style> |
| {% endblock %} |
|
|
| {% block body %} |
| <div class="page"> |
| <div class="container" style="max-width:1100px"> |
|
|
| |
| <div class="page-header" style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:1rem"> |
| <div> |
| <h1>🛠 Admin Dashboard</h1> |
| <p>Real-time database</p> |
| </div> |
| <span class="refresh-badge">Refresh in <span id="countdown">5</span>s</span> |
| </div> |
|
|
| |
| <div class="stats-grid"> |
| <div class="stat-card"> |
| <div class="val">{{ stats.total_users }}</div> |
| <div class="lbl">Users</div> |
| </div> |
| <div class="stat-card active-card"> |
| <div class="val">{{ stats.active_now }}</div> |
| <div class="lbl">Currently online</div> |
| </div> |
| <div class="stat-card"> |
| <div class="val">{{ stats.total_sessions }}</div> |
| <div class="lbl">Total sessions</div> |
| </div> |
| <div class="stat-card"> |
| <div class="val">{{ stats.total_preds }}</div> |
| <div class="lbl">Prédictions</div> |
| </div> |
| </div> |
|
|
| |
| <div class="card" style="margin-bottom:1.5rem"> |
| <p class="section-title">👤 Users Table</p> |
| <div class="overflow-x"> |
| <table class="admin-table"> |
| <thead> |
| <tr> |
| <th>ID</th> |
| <th>Username</th> |
| <th>Email</th> |
| <th>Create at </th> |
| </tr> |
| </thead> |
| <tbody> |
| {% for u in users %} |
| <tr> |
| <td class="mono muted">{{ u.id }}</td> |
| <td><strong>{{ u.username }}</strong></td> |
| <td class="muted">{{ u.email }}</td> |
| <td class="muted">{{ u.created[:19].replace('T', ' ') }}</td> |
| </tr> |
| {% else %} |
| <tr><td colspan="4" class="muted" style="text-align:center;padding:1.5rem">No users</td></tr> |
| {% endfor %} |
| </tbody> |
| </table> |
| </div> |
| </div> |
|
|
| |
| <div class="card"> |
| <p class="section-title">🔐 Table sessions — logins/logouts (last 50)</p> |
| <div class="overflow-x"> |
| <table class="admin-table"> |
| <thead> |
| <tr> |
| <th>ID</th> |
| <th>User</th> |
| <th>Email</th> |
| <th>Last logged in on</th> |
| <th>Logged out on</th> |
| <th>Status</th> |
| </tr> |
| </thead> |
| <tbody> |
| {% for s in sessions_log %} |
| <tr> |
| <td class="mono muted">{{ s.id }}</td> |
| <td><strong>{{ s.username }}</strong></td> |
| <td class="muted">{{ s.email }}</td> |
| <td class="muted">{{ s.login_at[:19].replace('T', ' ') }}</td> |
| <td class="muted"> |
| {% if s.logout_at %} |
| {{ s.logout_at[:19].replace('T', ' ') }} |
| {% else %} |
| <span class="muted">—</span> |
| {% endif %} |
| </td> |
| <td> |
| {% if s.logout_at %} |
| <span class="dot dot-gray"></span><span class="muted" style="font-size:.8rem">Disconnect</span> |
| {% else %} |
| <span class="dot dot-green"></span><span style="color:#4ade80;font-size:.8rem">On line</span> |
| {% endif %} |
| </td> |
| </tr> |
| {% else %} |
| <tr><td colspan="6" class="muted" style="text-align:center;padding:1.5rem">No sessions saved</td></tr> |
| {% endfor %} |
| </tbody> |
| </table> |
| </div> |
| </div> |
|
|
| </div> |
| </div> |
|
|
| <script> |
| |
| let count = 5; |
| const el = document.getElementById('countdown'); |
| setInterval(() => { |
| count--; |
| el.textContent = count; |
| if (count <= 0) { |
| window.location.reload(); |
| } |
| }, 1000); |
| </script> |
| {% endblock %} |
|
|