270 lines
11 KiB
HTML
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 & 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 %}
|