Files
boker/boker/templates/admin/dashboard.html
T
2026-06-24 23:15:13 -07:00

270 lines
11 KiB
HTML

{% extends "admin/base.html" %}
{% block title %}Dashboard{% endblock %}
{% block page_title %}Dashboard{% endblock %}
{% block content %}
<div class="adm-stat-section">
<div class="adm-stat-section__label">Users</div>
<div class="adm-stat-section__grid">
<div class="adm-stat">
<div class="adm-stat__label">Total users</div>
<div class="adm-stat__val">{{ stats.total_users }}</div>
<div class="adm-stat__sub"><b>{{ stats.new_users_7d }}</b> new this week · <b>{{ stats.new_users_30d }}</b> this month</div>
</div>
<div class="adm-stat">
<div class="adm-stat__label">Active accounts</div>
<div class="adm-stat__val">{{ stats.active_users }}</div>
<div class="adm-stat__sub"><b>{{ stats.disabled_users }}</b> disabled · <b>{{ stats.admin_count }}</b> admin{% if stats.admin_count != 1 %}s{% endif %}</div>
</div>
<div class="adm-stat adm-stat--accent">
<div class="adm-stat__label">Daily active</div>
<div class="adm-stat__val">{{ stats.dau }}</div>
<div class="adm-stat__sub">logged in today</div>
</div>
<div class="adm-stat adm-stat--accent">
<div class="adm-stat__label">Weekly active</div>
<div class="adm-stat__val">{{ stats.wau }}</div>
<div class="adm-stat__sub">past 7 days</div>
</div>
<div class="adm-stat adm-stat--accent">
<div class="adm-stat__label">Monthly active</div>
<div class="adm-stat__val">{{ stats.mau }}</div>
<div class="adm-stat__sub">past 30 days</div>
</div>
</div>
</div>
<div class="adm-stat-section">
<div class="adm-stat-section__label">Leagues</div>
<div class="adm-stat-section__grid">
<div class="adm-stat adm-stat--pos">
<div class="adm-stat__label">Active leagues</div>
<div class="adm-stat__val">{{ stats.active_leagues }}</div>
<div class="adm-stat__sub"><b>{{ stats.public_leagues }}</b> public · <b>{{ stats.new_leagues_30d }}</b> new this month</div>
</div>
<div class="adm-stat adm-stat--pos">
<div class="adm-stat__label">Avg sessions / league</div>
<div class="adm-stat__val">{{ stats.avg_sessions_per_league }}</div>
<div class="adm-stat__sub">across all leagues</div>
</div>
<div class="adm-stat adm-stat--pos">
<div class="adm-stat__label">Avg buyin / league</div>
<div class="adm-stat__val">{{ stats.avg_buyin_cents | money }}</div>
<div class="adm-stat__sub">total buyins per league</div>
</div>
</div>
</div>
<div class="adm-stat-section">
<div class="adm-stat-section__label">Activity</div>
<div class="adm-stat-section__grid">
<div class="adm-stat adm-stat--warn">
<div class="adm-stat__label">Sessions</div>
<div class="adm-stat__val">{{ stats.total_sessions }}</div>
<div class="adm-stat__sub"><b>{{ stats.open_sessions }}</b> currently open</div>
</div>
<div class="adm-stat adm-stat--warn">
<div class="adm-stat__label">Ledger events</div>
<div class="adm-stat__val">{{ stats.total_events }}</div>
<div class="adm-stat__sub">across all leagues</div>
</div>
</div>
</div>
<div class="adm-chart-row">
<div class="adm-chart-card">
<div class="adm-chart-card__head">
<div>
<div class="adm-chart-card__title">User activity</div>
<div class="adm-chart-card__sub">Logins &amp; new signups — last 30 days</div>
</div>
<span class="adm-chart-card__meta">DAU <b>{{ stats.dau }}</b> · MAU <b>{{ stats.mau }}</b></span>
</div>
<canvas id="userActivityChart" height="110"></canvas>
</div>
<div class="adm-chart-card">
<div class="adm-chart-card__head">
<div>
<div class="adm-chart-card__title">Session frequency</div>
<div class="adm-chart-card__sub">Sessions played per day — last 30 days</div>
</div>
<span class="adm-chart-card__meta">Total <b>{{ stats.total_sessions }}</b></span>
</div>
<canvas id="sessionActivityChart" height="110"></canvas>
</div>
</div>
<div class="adm-section">
<div class="adm-section__head">
<span class="adm-section__title">Recent sign-ups</span>
<a class="adm-btn adm-btn--ghost adm-btn--sm" href="{{ url_for('site_admin.users') }}">View all users</a>
</div>
{% if recent_users %}
<div class="adm-table-wrap">
<table class="adm-table">
<thead>
<tr>
<th>Email</th>
<th>Joined</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{% for user in recent_users %}
<tr>
<td><a href="{{ url_for('site_admin.user_detail', user_id=user.id) }}">{{ user.email }}</a>{% if user.is_site_admin %} <span class="adm-pill adm-pill--admin">admin</span>{% endif %}</td>
<td class="cell-muted">{{ user.created_at.strftime('%b %d, %Y') }}</td>
<td>
{% if user.disabled_at %}
<span class="adm-pill adm-pill--disabled">Disabled</span>
{% else %}
<span class="adm-pill adm-pill--active">Active</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p style="color:var(--faintest);font-size:13px;">No users yet.</p>
{% endif %}
</div>
<div class="adm-section">
<div class="adm-section__head">
<span class="adm-section__title">Recently created leagues</span>
<a class="adm-btn adm-btn--ghost adm-btn--sm" href="{{ url_for('site_admin.leagues') }}">View all leagues</a>
</div>
{% if recent_leagues %}
<div class="adm-table-wrap">
<table class="adm-table">
<thead>
<tr>
<th>Name</th>
<th>Owner</th>
<th>Visibility</th>
<th>Created</th>
</tr>
</thead>
<tbody>
{% for league in recent_leagues %}
<tr>
<td><a href="{{ url_for('site_admin.league_detail', league_id=league.id) }}">{{ league.name }}</a>{% if league.archived_at %} <span class="adm-pill adm-pill--archived">archived</span>{% endif %}</td>
<td>
{% set owner = owners_by_id.get(league.created_by_user_id) %}
{% if owner %}
<a href="{{ url_for('site_admin.user_detail', user_id=owner.id) }}">{{ owner.email }}</a>
{% else %}
<span class="cell-muted"></span>
{% endif %}
</td>
<td>
{% if league.visibility == 'public' %}
<span class="adm-pill adm-pill--public">Public</span>
{% else %}
<span class="adm-pill adm-pill--private">Private</span>
{% endif %}
</td>
<td class="cell-muted">{{ league.created_at.strftime('%b %d, %Y') }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p style="color:var(--faintest);font-size:13px;">No leagues yet.</p>
{% endif %}
</div>
{% endblock %}
{% block extra_scripts %}
<script>
(function() {
var d = {{ chart_data | safe }};
var style = getComputedStyle(document.documentElement);
var accent = style.getPropertyValue('--accent').trim() || '#9b8cf0';
var pos = style.getPropertyValue('--pos').trim() || '#6fc093';
var warn = style.getPropertyValue('--warn').trim() || '#e0b15c';
var faint = style.getPropertyValue('--faintest').trim() || '#66646f';
var border = style.getPropertyValue('--border').trim() || '#26262f';
Chart.defaults.color = faint;
Chart.defaults.font.family = 'inherit';
Chart.defaults.font.size = 11;
new Chart(document.getElementById('userActivityChart'), {
type: 'line',
data: {
labels: d.labels,
datasets: [
{
label: 'Active users',
data: d.logins,
borderColor: accent,
backgroundColor: accent + '18',
fill: true,
tension: 0.35,
pointRadius: 0,
pointHoverRadius: 4,
borderWidth: 2,
},
{
label: 'New signups',
data: d.signups,
borderColor: pos,
backgroundColor: 'transparent',
fill: false,
tension: 0.35,
pointRadius: 0,
pointHoverRadius: 4,
borderWidth: 1.5,
borderDash: [5, 3],
}
]
},
options: {
responsive: true,
interaction: { mode: 'index', intersect: false },
plugins: {
legend: { position: 'top', labels: { boxWidth: 10, boxHeight: 10, padding: 14, usePointStyle: true } }
},
scales: {
x: { grid: { display: false }, ticks: { maxTicksLimit: 7, color: faint } },
y: { beginAtZero: true, grid: { color: border }, ticks: { precision: 0, color: faint } }
}
}
});
new Chart(document.getElementById('sessionActivityChart'), {
type: 'bar',
data: {
labels: d.labels,
datasets: [{
label: 'Sessions',
data: d.sessions,
backgroundColor: warn + '55',
borderColor: warn + 'bb',
borderWidth: 1,
borderRadius: 3,
borderSkipped: false,
}]
},
options: {
responsive: true,
interaction: { mode: 'index', intersect: false },
plugins: {
legend: { position: 'top', labels: { boxWidth: 10, boxHeight: 10, padding: 14, usePointStyle: true } }
},
scales: {
x: { grid: { display: false }, ticks: { maxTicksLimit: 7, color: faint } },
y: { beginAtZero: true, grid: { color: border }, ticks: { precision: 0, color: faint } }
}
}
});
})();
</script>
{% endblock %}