Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>God Mode β Proofly Admin</title> | |
| <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}"> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&display=swap" | |
| rel="stylesheet"> | |
| <script src="https://unpkg.com/@phosphor-icons/web"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.2/dist/chart.umd.min.js"></script> | |
| <script>if (localStorage.getItem('proofly-theme') === 'dark') document.documentElement.setAttribute('data-theme', 'dark');</script> | |
| <style> | |
| /* ββ Layout ββ */ | |
| .admin-wrap { | |
| padding: 2rem 2.5rem; | |
| max-width: 1400px; | |
| margin: 0 auto; | |
| } | |
| .page-heading { | |
| font-size: 1.6rem; | |
| font-weight: 800; | |
| color: var(--text-main); | |
| margin-bottom: 0.25rem; | |
| display: flex; | |
| align-items: center; | |
| gap: 0.75rem; | |
| } | |
| .page-sub { | |
| font-size: 0.9rem; | |
| color: var(--text-muted); | |
| margin-bottom: 2rem; | |
| } | |
| .badge-admin { | |
| background: rgba(37, 99, 235, 0.12); | |
| color: #2563eb; | |
| padding: 0.25rem 0.75rem; | |
| border-radius: 20px; | |
| font-size: 0.75rem; | |
| font-weight: 700; | |
| letter-spacing: 0.05em; | |
| } | |
| /* ββ KPI Strip ββ */ | |
| .kpi-strip { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); | |
| gap: 1rem; | |
| margin-bottom: 2rem; | |
| } | |
| .kpi { | |
| background: var(--bg-card); | |
| border: 1px solid var(--border-color); | |
| border-radius: var(--radius-md); | |
| padding: 1.25rem 1.5rem; | |
| position: relative; | |
| overflow: hidden; | |
| transition: transform 0.15s, box-shadow 0.15s; | |
| } | |
| .kpi:hover { | |
| transform: translateY(-3px); | |
| box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08); | |
| } | |
| .kpi::after { | |
| content: ''; | |
| position: absolute; | |
| inset-block: 0; | |
| right: 0; | |
| width: 3px; | |
| background: var(--kpi-color, var(--primary)); | |
| border-radius: 3px 0 0 3px; | |
| } | |
| .kpi-label { | |
| font-size: 0.72rem; | |
| text-transform: uppercase; | |
| letter-spacing: 0.07em; | |
| color: var(--text-muted); | |
| font-weight: 600; | |
| margin-bottom: 0.4rem; | |
| } | |
| .kpi-value { | |
| font-size: 2rem; | |
| font-weight: 800; | |
| color: var(--text-main); | |
| line-height: 1; | |
| } | |
| .kpi-icon { | |
| position: absolute; | |
| top: 1rem; | |
| right: 1.25rem; | |
| font-size: 1.4rem; | |
| opacity: 0.15; | |
| } | |
| .kpi-sub { | |
| font-size: 0.78rem; | |
| color: var(--text-muted); | |
| margin-top: 0.3rem; | |
| } | |
| /* ββ Chart Grid ββ */ | |
| .chart-grid { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 1.5rem; | |
| margin-bottom: 2rem; | |
| } | |
| .chart-grid-3 { | |
| display: grid; | |
| grid-template-columns: 2fr 1fr; | |
| gap: 1.5rem; | |
| margin-bottom: 2rem; | |
| } | |
| .chart-card { | |
| background: var(--bg-card); | |
| border: 1px solid var(--border-color); | |
| border-radius: var(--radius-md); | |
| padding: 1.5rem; | |
| } | |
| .chart-card-title { | |
| font-size: 0.85rem; | |
| font-weight: 700; | |
| color: var(--text-muted); | |
| text-transform: uppercase; | |
| letter-spacing: 0.06em; | |
| margin-bottom: 1.25rem; | |
| } | |
| .chart-canvas-wrap { | |
| position: relative; | |
| } | |
| /* ββ Activity Table ββ */ | |
| .activity-section { | |
| background: var(--bg-card); | |
| border: 1px solid var(--border-color); | |
| border-radius: var(--radius-md); | |
| padding: 1.5rem; | |
| margin-bottom: 2rem; | |
| } | |
| .section-hdr { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 1.25rem; | |
| } | |
| .section-title { | |
| font-size: 1.05rem; | |
| font-weight: 700; | |
| color: var(--text-main); | |
| } | |
| .view-all-btn { | |
| font-size: 0.82rem; | |
| color: var(--primary); | |
| text-decoration: none; | |
| font-weight: 600; | |
| display: flex; | |
| align-items: center; | |
| gap: 0.3rem; | |
| } | |
| .view-all-btn:hover { | |
| opacity: 0.7; | |
| } | |
| .admin-table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| font-size: 0.875rem; | |
| } | |
| .admin-table th { | |
| text-align: left; | |
| padding: 0.6rem 1rem; | |
| border-bottom: 2px solid var(--border-color); | |
| color: var(--text-muted); | |
| font-size: 0.72rem; | |
| text-transform: uppercase; | |
| letter-spacing: 0.06em; | |
| } | |
| .admin-table td { | |
| padding: 0.9rem 1rem; | |
| border-bottom: 1px solid var(--border-color); | |
| color: var(--text-main); | |
| } | |
| .admin-table tr:last-child td { | |
| border-bottom: none; | |
| } | |
| .admin-table tr:hover td { | |
| background: var(--bg-input); | |
| } | |
| .badge { | |
| padding: 0.2rem 0.6rem; | |
| border-radius: 20px; | |
| font-size: 0.72rem; | |
| font-weight: 700; | |
| } | |
| .badge-true, | |
| .badge-real { | |
| background: rgba(16, 185, 129, 0.1); | |
| color: #10b981; | |
| } | |
| .badge-false, | |
| .badge-fake { | |
| background: rgba(239, 68, 68, 0.1); | |
| color: #ef4444; | |
| } | |
| .badge-uncertain { | |
| background: rgba(245, 158, 11, 0.1); | |
| color: #f59e0b; | |
| } | |
| .badge-user-role { | |
| background: var(--bg-input); | |
| color: var(--text-muted); | |
| } | |
| /* ββ Sidebar ββ */ | |
| .admin-sidebar .nav-btn { | |
| color: var(--text-muted); | |
| } | |
| .admin-sidebar .active-icon { | |
| color: var(--primary); | |
| } | |
| @media (max-width: 900px) { | |
| .chart-grid, | |
| .chart-grid-3 { | |
| grid-template-columns: 1fr; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="app-container"> | |
| <!-- Sidebar --> | |
| <aside class="sidebar admin-sidebar"> | |
| <div class="sidebar-top"> | |
| <a href="/" class="nav-btn" title="Back to Home" style="text-decoration:none;"><i | |
| class="ph ph-house"></i></a> | |
| <div class="spacer"></div> | |
| <a href="/admin" class="icon-btn active-icon" title="Dashboard" style="text-decoration:none;"><i | |
| class="ph ph-chart-pie-slice"></i></a> | |
| <a href="/admin/users" class="nav-btn" title="Users" style="text-decoration:none;"><i | |
| class="ph ph-users"></i></a> | |
| <a href="/admin/logs" class="nav-btn" title="Logs" style="text-decoration:none;"><i | |
| class="ph ph-rows"></i></a> | |
| </div> | |
| <div class="sidebar-bottom"> | |
| <button class="theme-toggle-btn" title="Toggle theme" onclick="toggleTheme()"> | |
| <i class="ph ph-moon icon-moon"></i> | |
| <i class="ph ph-sun icon-sun"></i> | |
| </button> | |
| </div> | |
| </aside> | |
| <!-- Main --> | |
| <main class="main-content"> | |
| <header class="top-header"> | |
| <div class="header-center"><span class="daily-text"><i class="ph ph-shield-check" | |
| style="color:var(--primary);margin-right:0.5rem;"></i>God Mode</span></div> | |
| </header> | |
| <div class="admin-wrap"> | |
| <div class="page-heading"> | |
| System Overview <span class="badge-admin">ADMIN</span> | |
| </div> | |
| <p class="page-sub">Real-time intelligence across all Proofly users and claims.</p> | |
| <!-- KPI Cards --> | |
| <div class="kpi-strip"> | |
| <div class="kpi" style="--kpi-color: #2563eb;"> | |
| <div class="kpi-label">Total Users</div> | |
| <div class="kpi-value">{{ stats.total_users }}</div> | |
| <i class="ph ph-users kpi-icon"></i> | |
| </div> | |
| <div class="kpi" style="--kpi-color: #10b981;"> | |
| <div class="kpi-label">Total Checks</div> | |
| <div class="kpi-value">{{ stats.total_checks }}</div> | |
| <i class="ph ph-magnifying-glass kpi-icon"></i> | |
| </div> | |
| <div class="kpi" style="--kpi-color: #8b5cf6;"> | |
| <div class="kpi-label">Checks Today</div> | |
| <div class="kpi-value">{{ stats.recent_checks_24h }}</div> | |
| <div class="kpi-sub">Last 24 hours</div> | |
| <i class="ph ph-lightning kpi-icon"></i> | |
| </div> | |
| <div class="kpi" style="--kpi-color: #f59e0b;"> | |
| <div class="kpi-label">Cache Hit Rate</div> | |
| <div class="kpi-value">{{ stats.cache_hit_rate }}%</div> | |
| <div class="kpi-sub">{{ stats.total_cached }} results cached</div> | |
| <i class="ph ph-database kpi-icon"></i> | |
| </div> | |
| <div class="kpi" style="--kpi-color: #ec4899;"> | |
| <div class="kpi-label">Evidence Docs</div> | |
| <div class="kpi-value">{{ stats.total_evidence }}</div> | |
| <i class="ph ph-file-text kpi-icon"></i> | |
| </div> | |
| </div> | |
| <!-- Chart Row 1: Daily Trend + Verdict Doughnut --> | |
| <div class="chart-grid"> | |
| <div class="chart-card"> | |
| <div class="chart-card-title">Daily Fact Checks β Last 7 Days</div> | |
| <div class="chart-canvas-wrap"> | |
| <canvas id="dailyChart" height="120"></canvas> | |
| </div> | |
| </div> | |
| <div class="chart-card"> | |
| <div class="chart-card-title">Verdict Breakdown</div> | |
| <div class="chart-canvas-wrap"> | |
| <canvas id="verdictChart" height="120"></canvas> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Chart Row 2: Top Users + Cache Gauge --> | |
| <div class="chart-grid-3"> | |
| <div class="chart-card"> | |
| <div class="chart-card-title">Top Users by Activity</div> | |
| <div class="chart-canvas-wrap"> | |
| <canvas id="topUsersChart" height="120"></canvas> | |
| </div> | |
| </div> | |
| <div class="chart-card"> | |
| <div class="chart-card-title">Cache Efficiency</div> | |
| <div class="chart-canvas-wrap" | |
| style="display:flex; align-items:center; justify-content:center; flex-direction:column; padding-top: 1rem;"> | |
| <canvas id="cacheGaugeChart" height="120" style="max-width: 220px;"></canvas> | |
| <p | |
| style="color: var(--text-muted); font-size: 0.85rem; margin-top: 1rem; text-align:center;"> | |
| {{ stats.cache_hit_rate }}% of all checks served from cache</p> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Live Activity Feed --> | |
| <div class="activity-section"> | |
| <div class="section-hdr"> | |
| <span class="section-title">Live Activity Feed</span> | |
| <a href="/admin/logs" class="view-all-btn">View all <i class="ph ph-arrow-right"></i></a> | |
| </div> | |
| <table class="admin-table"> | |
| <thead> | |
| <tr> | |
| <th>User</th> | |
| <th>Claim</th> | |
| <th>Verdict</th> | |
| <th>Confidence</th> | |
| <th>Time</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {% for item in history %} | |
| <tr> | |
| <td><strong>{{ item.username }}</strong></td> | |
| <td | |
| style="max-width: 320px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;"> | |
| {{ item.claim }}</td> | |
| <td> | |
| <span class="badge badge-{{ item.verdict|lower }}">{{ item.verdict }}</span> | |
| </td> | |
| <td>{{ (item.confidence * 100)|round(1) }}%</td> | |
| <td style="color: var(--text-muted); font-size: 0.82rem; white-space: nowrap;">{{ | |
| item.created_at.strftime('%b %d, %H:%M') }}</td> | |
| </tr> | |
| {% endfor %} | |
| </tbody> | |
| </table> | |
| </div> | |
| <!-- Newest Users --> | |
| <div class="activity-section"> | |
| <div class="section-hdr"> | |
| <span class="section-title">User Registry</span> | |
| <a href="/admin/users" class="view-all-btn">Manage all <i class="ph ph-arrow-right"></i></a> | |
| </div> | |
| <table class="admin-table"> | |
| <thead> | |
| <tr> | |
| <th>Username</th> | |
| <th>Email</th> | |
| <th>Role</th> | |
| <th>Joined</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {% for user in users %} | |
| <tr> | |
| <td>{{ user.username }}</td> | |
| <td style="color: var(--text-muted);">{{ user.email }}</td> | |
| <td> | |
| {% if user.is_admin %} | |
| <span class="badge badge-admin">ADMIN</span> | |
| {% else %} | |
| <span class="badge badge-user-role">USER</span> | |
| {% endif %} | |
| </td> | |
| <td style="color: var(--text-muted); font-size: 0.82rem;">{{ | |
| user.created_at.strftime('%Y-%m-%d') }}</td> | |
| </tr> | |
| {% endfor %} | |
| </tbody> | |
| </table> | |
| </div> | |
| </div><!-- /admin-wrap --> | |
| </main> | |
| </div> | |
| <script> | |
| // ββ Theme ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function toggleTheme() { | |
| const isDark = document.documentElement.getAttribute('data-theme') === 'dark'; | |
| if (isDark) { | |
| document.documentElement.removeAttribute('data-theme'); | |
| localStorage.setItem('proofly-theme', 'light'); | |
| } else { | |
| document.documentElement.setAttribute('data-theme', 'dark'); | |
| localStorage.setItem('proofly-theme', 'dark'); | |
| } | |
| // Re-render charts with new theme colours | |
| location.reload(); | |
| } | |
| // ββ Chart Defaults βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const isDark = document.documentElement.getAttribute('data-theme') === 'dark'; | |
| const textColor = isDark ? '#94a3b8' : '#64748b'; | |
| const gridColor = isDark ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.06)'; | |
| Chart.defaults.font.family = 'Outfit, sans-serif'; | |
| Chart.defaults.color = textColor; | |
| // ββ 1. Daily Checks Line Chart βββββββββββββββββββββββββββββββββββββββββββββ | |
| const dailyCtx = document.getElementById('dailyChart').getContext('2d'); | |
| const dailyGrad = dailyCtx.createLinearGradient(0, 0, 0, 220); | |
| dailyGrad.addColorStop(0, 'rgba(37,99,235,0.25)'); | |
| dailyGrad.addColorStop(1, 'rgba(37,99,235,0.0)'); | |
| new Chart(dailyCtx, { | |
| type: 'line', | |
| data: { | |
| labels: {{ stats.daily_labels | tojson }}, | |
| datasets: [{ | |
| label: 'Checks', | |
| data: {{ stats.daily_data | tojson }}, | |
| borderColor: '#2563eb', | |
| backgroundColor: dailyGrad, | |
| borderWidth: 2.5, | |
| pointRadius: 4, | |
| pointBackgroundColor: '#2563eb', | |
| fill: true, | |
| tension: 0.4, | |
| }] | |
| }, | |
| options: { | |
| responsive: true, | |
| scales: { | |
| x: { grid: { color: gridColor }, ticks: { color: textColor } }, | |
| y: { grid: { color: gridColor }, ticks: { color: textColor, stepSize: 1 }, beginAtZero: true }, | |
| }, | |
| plugins: { legend: { display: false }, tooltip: { mode: 'index', intersect: false } } | |
| } | |
| }); | |
| // ββ 2. Verdict Doughnut βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const vc = {{ stats.verdict_counts | tojson }}; | |
| const verdictLabels = Object.keys(vc).length ? Object.keys(vc) : ['No Data']; | |
| const verdictValues = Object.values(vc).length ? Object.values(vc) : [1]; | |
| const verdictColors = verdictLabels.map(l => { | |
| if (l === 'TRUE') return '#10b981'; | |
| if (l === 'FALSE') return '#ef4444'; | |
| if (l === 'UNCERTAIN') return '#f59e0b'; | |
| return '#6366f1'; | |
| }); | |
| new Chart(document.getElementById('verdictChart'), { | |
| type: 'doughnut', | |
| data: { labels: verdictLabels, datasets: [{ data: verdictValues, backgroundColor: verdictColors, borderWidth: 0, hoverOffset: 8 }] }, | |
| options: { | |
| cutout: '70%', | |
| plugins: { legend: { position: 'bottom', labels: { padding: 16, boxWidth: 12 } } } | |
| } | |
| }); | |
| // ββ 3. Top Users Horizontal Bar ββββββββββββββββββββββββββββββββββββββββββββ | |
| const topUsers = {{ stats.top_users | tojson }}; | |
| new Chart(document.getElementById('topUsersChart'), { | |
| type: 'bar', | |
| data: { | |
| labels: topUsers.map(u => u.username), | |
| datasets: [{ | |
| label: 'Checks', | |
| data: topUsers.map(u => u.count), | |
| backgroundColor: ['#2563eb', '#8b5cf6', '#10b981', '#f59e0b', '#ec4899'], | |
| borderRadius: 6, | |
| }] | |
| }, | |
| options: { | |
| indexAxis: 'y', | |
| responsive: true, | |
| scales: { | |
| x: { grid: { color: gridColor }, ticks: { color: textColor, stepSize: 1 }, beginAtZero: true }, | |
| y: { grid: { display: false }, ticks: { color: textColor } }, | |
| }, | |
| plugins: { legend: { display: false } } | |
| } | |
| }); | |
| // ββ 4. Cache Efficiency Doughnut βββββββββββββββββββββββββββββββββββββββββββ | |
| const cacheRate = {{ stats.cache_hit_rate }}; | |
| new Chart(document.getElementById('cacheGaugeChart'), { | |
| type: 'doughnut', | |
| data: { | |
| labels: ['Cached', 'Live API'], | |
| datasets: [{ | |
| data: [cacheRate, Math.max(0, 100 - cacheRate)], | |
| backgroundColor: ['#10b981', isDark ? 'rgba(255,255,255,0.07)' : 'rgba(0,0,0,0.07)'], | |
| borderWidth: 0, | |
| hoverOffset: 4, | |
| }] | |
| }, | |
| options: { | |
| cutout: '75%', | |
| plugins: { legend: { position: 'bottom', labels: { padding: 14, boxWidth: 10 } } } | |
| } | |
| }); | |
| </script> | |
| </body> | |
| </html> |