proofly / templates /admin.html
Pragthedon's picture
Initial backend API deployment
4f48a4e
<!DOCTYPE html>
<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>