?{% extends "base.twig" %}
{% block title %}{{ t('ui.admin.activity_stats.page_title') }}{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2">{{ t('ui.admin.activity_stats.heading') }}</h1>
<div class="d-flex align-items-center gap-2">
<label class="me-1 mb-0 text-muted small">{{ t('ui.admin.activity_stats.period') }}:</label>
<select id="period-select" class="form-select form-select-sm" style="width:auto">
<option value="7d">{{ t('ui.admin.activity_stats.period_7d') }}</option>
<option value="30d" selected>{{ t('ui.admin.activity_stats.period_30d') }}</option>
<option value="90d">{{ t('ui.admin.activity_stats.period_90d') }}</option>
<option value="all">{{ t('ui.admin.activity_stats.period_all') }}</option>
</select>
<div class="form-check mb-0 ms-2">
<input class="form-check-input" type="checkbox" id="exclude-admins">
<label class="form-check-label text-muted small" for="exclude-admins">{{ t('ui.admin.activity_stats.exclude_admins') }}</label>
</div>
<a href="/admin" class="btn btn-sm btn-outline-secondary">
<i class="fas fa-arrow-left me-1"></i>{{ t('ui.admin.activity_stats.dashboard') }}
</a>
</div>
</div>
<div id="loading-indicator" class="text-center py-5">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">{{ t('ui.common.loading') }}</span>
</div>
<p class="mt-2 text-muted">{{ t('ui.admin.activity_stats.loading_activity_data') }}</p>
</div>
<div id="stats-content" style="display:none">
<!-- Summary Cards -->
<div class="row mb-4" id="summary-cards"></div>
<!-- Tabs -->
<ul class="nav nav-tabs mb-3" id="stats-tabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="tab-overview" data-bs-toggle="tab" data-bs-target="#pane-overview" type="button">{{ t('ui.admin.activity_stats.tab_overview') }}</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="tab-areas" data-bs-toggle="tab" data-bs-target="#pane-areas" type="button">{{ t('ui.admin.activity_stats.tab_popular_areas') }}</button>
</li>
{% if interests_enabled %}
<li class="nav-item" role="presentation">
<button class="nav-link" id="tab-interests" data-bs-toggle="tab" data-bs-target="#pane-interests" type="button">{{ t('ui.admin.activity_stats.tab_interests') }}</button>
</li>
{% endif %}
<li class="nav-item" role="presentation">
<button class="nav-link" id="tab-doors" data-bs-toggle="tab" data-bs-target="#pane-doors" type="button">{{ t('ui.admin.activity_stats.tab_doors') }}</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="tab-files" data-bs-toggle="tab" data-bs-target="#pane-files" type="button">{{ t('ui.admin.activity_stats.tab_file_activity') }}</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="tab-nodelist" data-bs-toggle="tab" data-bs-target="#pane-nodelist" type="button">{{ t('ui.admin.activity_stats.tab_nodelist') }}</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="tab-users" data-bs-toggle="tab" data-bs-target="#pane-users" type="button">{{ t('ui.admin.activity_stats.tab_top_users') }}</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="tab-hourly" data-bs-toggle="tab" data-bs-target="#pane-hourly" type="button">{{ t('ui.admin.activity_stats.tab_hourly') }}</button>
</li>
</ul>
<div class="tab-content" id="stats-tab-content">
<!-- Overview Tab -->
<div class="tab-pane fade show active" id="pane-overview" role="tabpanel">
<div class="row">
<div class="col-lg-6 mb-4">
<div class="card shadow-sm">
<div class="card-header"><strong>{{ t('ui.admin.activity_stats.activity_by_category') }}</strong></div>
<div class="card-body p-0">
<table class="table table-sm table-hover mb-0">
<thead><tr><th>{{ t('ui.admin.activity_stats.category') }}</th><th class="text-end">{{ t('ui.admin.activity_stats.events') }}</th><th style="width:40%"></th></tr></thead>
<tbody id="category-rows"></tbody>
</table>
</div>
</div>
</div>
<div class="col-lg-6 mb-4">
<div class="card shadow-sm">
<div class="card-header"><strong>{{ t('ui.admin.activity_stats.daily_activity_last_30') }}</strong></div>
<div class="card-body p-0">
<table class="table table-sm table-hover mb-0" id="daily-table">
<thead><tr><th>{{ t('ui.admin.activity_stats.date') }}</th><th class="text-end">{{ t('ui.admin.activity_stats.events') }}</th><th style="width:40%"></th></tr></thead>
<tbody id="daily-rows"></tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- Popular Areas Tab -->
<div class="tab-pane fade" id="pane-areas" role="tabpanel">
<div class="row">
<div class="col-lg-6 mb-4">
<div class="card shadow-sm">
<div class="card-header"><strong>{{ t('ui.admin.activity_stats.most_viewed_echoareas') }}</strong></div>
<div class="card-body p-0">
<table class="table table-sm table-hover mb-0">
<thead><tr><th>{{ t('ui.admin.activity_stats.area') }}</th><th class="text-end">{{ t('ui.admin.activity_stats.views') }}</th><th style="width:35%"></th></tr></thead>
<tbody id="echoarea-views-rows"></tbody>
</table>
</div>
</div>
</div>
<div class="col-lg-6 mb-4">
<div class="card shadow-sm">
<div class="card-header"><strong>{{ t('ui.admin.activity_stats.most_active_echoareas') }}</strong></div>
<div class="card-body p-0">
<table class="table table-sm table-hover mb-0">
<thead><tr><th>{{ t('ui.admin.activity_stats.area') }}</th><th class="text-end">{{ t('ui.admin.activity_stats.posts') }}</th><th style="width:35%"></th></tr></thead>
<tbody id="echoarea-posts-rows"></tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- Doors Tab -->
<div class="tab-pane fade" id="pane-doors" role="tabpanel">
<div class="row">
<div class="col-lg-6 mb-4">
<div class="card shadow-sm">
<div class="card-header"><strong>{{ t('ui.admin.activity_stats.most_played_webdoors') }}</strong></div>
<div class="card-body p-0">
<table class="table table-sm table-hover mb-0">
<thead><tr><th>{{ t('ui.admin.activity_stats.game') }}</th><th class="text-end">{{ t('ui.admin.activity_stats.sessions') }}</th><th style="width:35%"></th></tr></thead>
<tbody id="webdoor-rows"></tbody>
</table>
</div>
</div>
</div>
<div class="col-lg-6 mb-4">
<div class="card shadow-sm">
<div class="card-header"><strong>{{ t('ui.admin.activity_stats.most_played_dos_doors') }}</strong></div>
<div class="card-body p-0">
<table class="table table-sm table-hover mb-0">
<thead><tr><th>{{ t('ui.admin.activity_stats.door') }}</th><th class="text-end">{{ t('ui.admin.activity_stats.sessions') }}</th><th style="width:35%"></th></tr></thead>
<tbody id="dosdoor-rows"></tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- File Activity Tab -->
<div class="tab-pane fade" id="pane-files" role="tabpanel">
<div class="row">
<div class="col-lg-6 mb-4">
<div class="card shadow-sm">
<div class="card-header"><strong>{{ t('ui.admin.activity_stats.top_downloaded_files') }}</strong></div>
<div class="card-body p-0">
<table class="table table-sm table-hover mb-0">
<thead><tr><th>{{ t('ui.admin.activity_stats.file') }}</th><th class="text-end">{{ t('ui.admin.activity_stats.downloads') }}</th><th style="width:35%"></th></tr></thead>
<tbody id="file-download-rows"></tbody>
</table>
</div>
</div>
</div>
<div class="col-lg-6 mb-4">
<div class="card shadow-sm">
<div class="card-header"><strong>{{ t('ui.admin.activity_stats.most_browsed_file_areas') }}</strong></div>
<div class="card-body p-0">
<table class="table table-sm table-hover mb-0">
<thead><tr><th>{{ t('ui.admin.activity_stats.area') }}</th><th class="text-end">{{ t('ui.admin.activity_stats.views') }}</th><th style="width:35%"></th></tr></thead>
<tbody id="filearea-rows"></tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- Nodelist Tab -->
<div class="tab-pane fade" id="pane-nodelist" role="tabpanel">
<div class="row">
<div class="col-lg-6 mb-4">
<div class="card shadow-sm">
<div class="card-header"><strong>{{ t('ui.admin.activity_stats.most_searched_nodelist_queries') }}</strong></div>
<div class="card-body p-0">
<table class="table table-sm table-hover mb-0">
<thead><tr><th>{{ t('ui.admin.activity_stats.query') }}</th><th class="text-end">{{ t('ui.admin.activity_stats.searches') }}</th><th style="width:35%"></th></tr></thead>
<tbody id="nodelist-search-rows"></tbody>
</table>
</div>
</div>
</div>
<div class="col-lg-6 mb-4">
<div class="card shadow-sm">
<div class="card-header"><strong>{{ t('ui.admin.activity_stats.most_viewed_nodes') }}</strong></div>
<div class="card-body p-0">
<table class="table table-sm table-hover mb-0">
<thead><tr><th>{{ t('ui.admin.activity_stats.node') }}</th><th class="text-end">{{ t('ui.admin.activity_stats.views') }}</th><th style="width:35%"></th></tr></thead>
<tbody id="node-view-rows"></tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- Top Users Tab -->
<div class="tab-pane fade" id="pane-users" role="tabpanel">
<div class="card shadow-sm">
<div class="card-header"><strong>{{ t('ui.admin.activity_stats.most_active_users') }}</strong></div>
<div class="card-body p-0">
<table class="table table-sm table-hover mb-0">
<thead><tr><th>{{ t('ui.admin.activity_stats.user') }}</th><th class="text-end">{{ t('ui.admin.activity_stats.events') }}</th><th style="width:35%"></th></tr></thead>
<tbody id="top-users-rows"></tbody>
</table>
</div>
</div>
</div>
<!-- Hourly Distribution Tab -->
<div class="tab-pane fade" id="pane-hourly" role="tabpanel">
<div class="card shadow-sm">
<div class="card-header"><strong>{{ t('ui.admin.activity_stats.activity_by_hour_of_day') }} (<span id="hourly-tz-label">{{ t('ui.admin.activity_stats.utc', {}, locale, ['common']) }}</span>)</strong></div>
<div class="card-body p-0">
<table class="table table-sm table-hover mb-0">
<thead><tr><th>{{ t('ui.admin.activity_stats.hour') }} (<span class="hourly-tz-col">{{ t('ui.admin.activity_stats.utc', {}, locale, ['common']) }}</span>)</th><th class="text-end">{{ t('ui.admin.activity_stats.events') }}</th><th style="width:50%"></th></tr></thead>
<tbody id="hourly-rows"></tbody>
</table>
</div>
</div>
</div>
{% if interests_enabled %}
<!-- Popular Interests Tab -->
<div class="tab-pane fade" id="pane-interests" role="tabpanel">
<div class="card shadow-sm">
<div class="card-header"><strong>{{ t('ui.admin.activity_stats.popular_interests') }}</strong></div>
<div class="card-body p-0">
<table class="table table-sm table-hover mb-0">
<thead><tr><th>{{ t('ui.admin.activity_stats.interest') }}</th><th class="text-end">{{ t('ui.admin.activity_stats.subscribers') }}</th><th style="width:35%"></th></tr></thead>
<tbody id="interest-rows"></tbody>
</table>
</div>
</div>
</div>
{% endif %}
</div><!-- /.tab-content -->
</div><!-- /#stats-content -->
</div>
<script>
(function() {
const userTimezone = {{ user_timezone|json_encode(15)|raw }};
function uiT(key, fallback, params = {}) {
if (window.t) {
return window.t(key, params, fallback);
}
return fallback;
}
function formatDate(dateStr) {
const parts = dateStr.split('-');
const localDate = new Date(parts[0], parts[1] - 1, parts[2]);
return localDate.toLocaleDateString(undefined, { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric' });
}
function getTzAbbr(timezone) {
try {
return new Intl.DateTimeFormat(undefined, { timeZone: timezone, timeZoneName: 'short' })
.formatToParts(new Date())
.find(p => p.type === 'timeZoneName')?.value || timezone;
} catch (e) {
return timezone;
}
}
function escapeHtml(str) {
if (!str) return '—';
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
}
function apiError(payload, fallback) {
if (window.getApiErrorMessage) {
return window.getApiErrorMessage(payload, fallback);
}
return payload && payload.error ? payload.error : fallback;
}
function progressBar(value, max, cls) {
const pct = max > 0 ? Math.round((value / max) * 100) : 0;
return `<div class="progress" style="height:14px"><div class="progress-bar ${cls || ''}" style="width:${pct}%"></div></div>`;
}
function renderRows(tbodyId, items, nameFn, valueFn, barCls) {
const tbody = document.getElementById(tbodyId);
if (!tbody) return;
if (!items || items.length === 0) {
tbody.innerHTML = `<tr><td colspan="3" class="text-muted text-center py-3">${uiT('ui.admin.activity_stats.no_data_for_period', 'No data for this period')}</td></tr>`;
return;
}
const maxVal = Math.max(...items.map(r => parseInt(valueFn(r)) || 0));
tbody.innerHTML = items.map(r => {
const val = parseInt(valueFn(r)) || 0;
return `<tr>
<td class="text-truncate" style="max-width:180px">${escapeHtml(nameFn(r))}</td>
<td class="text-end fw-semibold">${val.toLocaleString()}</td>
<td>${progressBar(val, maxVal, barCls)}</td>
</tr>`;
}).join('');
}
function summaryCard(label, count, icon, cls, bg, sub) {
const subHtml = sub ? `<div class="text-muted" style="font-size:0.7rem;line-height:1.4">${sub}</div>` : '';
return `<div class="col-xl-2 col-md-4 col-sm-6 mb-4">
<div class="card shadow-sm h-100">
<div class="card-body d-flex align-items-center gap-3">
<div class="rounded-circle p-3 ${bg} bg-opacity-10 flex-shrink-0">
<i class="fas ${icon} fa-lg ${cls}"></i>
</div>
<div>
<div class="text-muted small text-uppercase">${label}</div>
<div class="h4 mb-0 fw-bold">${count.toLocaleString()}</div>
${subHtml}
</div>
</div>
</div>
</div>`;
}
function renderSummaryCards(data) {
const summary = data.summary || {};
const byCategory = summary.by_category || {};
const byType = summary.by_type || {};
const total = summary.total || 0;
const echoViews = byType.echomail_views || 0;
const echoSends = byType.echomail_sends || 0;
const netReads = byType.netmail_reads || 0;
const netSends = byType.netmail_sends || 0;
document.getElementById('summary-cards').innerHTML =
summaryCard(uiT('ui.admin.activity_stats.echomail', 'Echomail'), echoViews + echoSends, 'fa-comments', 'text-primary', 'bg-primary',
uiT('ui.admin.activity_stats.views_sent', 'Views: {views} · Sent: {sent}', { views: echoViews.toLocaleString(), sent: echoSends.toLocaleString() })) +
summaryCard(uiT('ui.admin.activity_stats.netmail', 'Netmail'), netReads + netSends, 'fa-envelope', 'text-secondary', 'bg-secondary',
uiT('ui.admin.activity_stats.read_sent', 'Read: {read} · Sent: {sent}', { read: netReads.toLocaleString(), sent: netSends.toLocaleString() })) +
summaryCard(uiT('ui.admin.activity_stats.files', 'Files'), byCategory.file || 0, 'fa-folder-open', 'text-success', 'bg-success') +
summaryCard(uiT('ui.admin.activity_stats.door_plays', 'Door Plays'), byCategory.door || 0, 'fa-gamepad', 'text-warning', 'bg-warning') +
summaryCard(uiT('ui.admin.activity_stats.logins', 'Logins'), byCategory.auth || 0, 'fa-sign-in-alt', 'text-info', 'bg-info') +
summaryCard(uiT('ui.admin.activity_stats.total', 'Total'), total, 'fa-chart-bar', 'text-secondary', 'bg-secondary');
}
function renderOverview(data) {
const byCategory = (data.summary || {}).by_category || {};
const byType = (data.summary || {}).by_type || {};
const loginBySource = (data.summary || {}).login_by_source || {};
const echoViews = byType.echomail_views || 0;
const echoSends = byType.echomail_sends || 0;
const netReads = byType.netmail_reads || 0;
const netSends = byType.netmail_sends || 0;
const allVals = [
echoViews + echoSends, echoViews, echoSends,
netReads + netSends, netReads, netSends,
byCategory.file || 0, byCategory.door || 0,
byCategory.nodelist || 0, byCategory.chat || 0, byCategory.auth || 0,
...Object.values(loginBySource),
];
const maxCat = Math.max(...allVals);
function groupRow(label, val) {
return `<tr class="fw-semibold">
<td>${label}</td>
<td class="text-end">${val.toLocaleString()}</td>
<td>${progressBar(val, maxCat, 'bg-primary')}</td>
</tr>`;
}
function subRow(label, val, barCls) {
return `<tr class="text-muted" style="font-size:0.875rem">
<td class="ps-4"><i class="fas fa-angle-right me-1 opacity-50"></i>${label}</td>
<td class="text-end">${val.toLocaleString()}</td>
<td>${progressBar(val, maxCat, barCls)}</td>
</tr>`;
}
function plainRow(label, val) {
return `<tr>
<td>${label}</td>
<td class="text-end fw-semibold">${val.toLocaleString()}</td>
<td>${progressBar(val, maxCat, 'bg-primary')}</td>
</tr>`;
}
const catTbody = document.getElementById('category-rows');
catTbody.innerHTML =
groupRow(uiT('ui.admin.activity_stats.echomail', 'Echomail'), echoViews + echoSends) +
subRow(uiT('ui.admin.activity_stats.area_views', 'Area Views'), echoViews, 'bg-primary') +
subRow(uiT('ui.admin.activity_stats.sent', 'Sent'), echoSends, 'bg-success') +
groupRow(uiT('ui.admin.activity_stats.netmail', 'Netmail'), netReads + netSends) +
subRow(uiT('ui.admin.activity_stats.read', 'Read'), netReads, 'bg-secondary') +
subRow(uiT('ui.admin.activity_stats.sent', 'Sent'), netSends, 'bg-success') +
plainRow(uiT('ui.admin.activity_stats.files', 'Files'), byCategory.file || 0) +
plainRow(uiT('ui.admin.activity_stats.doors', 'Doors'), byCategory.door || 0) +
plainRow(uiT('ui.admin.activity_stats.nodelist', 'Nodelist'), byCategory.nodelist || 0) +
plainRow(uiT('ui.admin.activity_stats.chat', 'Chat'), byCategory.chat || 0) +
groupRow(uiT('ui.admin.activity_stats.auth', 'Auth'), byCategory.auth || 0) +
Object.entries(loginBySource).map(([src, cnt]) =>
subRow(escapeHtml(src.charAt(0).toUpperCase() + src.slice(1)), cnt, 'bg-info')
).join('');
const daily = data.daily || [];
const dailyTbody = document.getElementById('daily-rows');
if (daily.length === 0) {
dailyTbody.innerHTML = `<tr><td colspan="3" class="text-muted text-center py-3">${uiT('ui.admin.activity_stats.no_data', 'No data')}</td></tr>`;
} else {
const maxDay = Math.max(...daily.map(d => d.count || 0));
dailyTbody.innerHTML = daily.slice().reverse().map(d =>
`<tr>
<td>${escapeHtml(formatDate(d.date))}</td>
<td class="text-end fw-semibold">${(d.count || 0).toLocaleString()}</td>
<td>${progressBar(d.count || 0, maxDay, 'bg-info')}</td>
</tr>`
).join('');
}
}
function renderAreas(data) {
const areas = data.popular_echoareas || [];
renderRows('echoarea-views-rows', areas, r => r.name, r => r.views, 'bg-primary');
renderRows('echoarea-posts-rows', areas.slice().sort((a, b) => (b.posts || 0) - (a.posts || 0)), r => r.name, r => r.posts, 'bg-success');
}
function renderDoors(data) {
renderRows('webdoor-rows', data.popular_webdoors || [], r => r.name, r => r.count, 'bg-warning');
renderRows('dosdoor-rows', data.popular_dosdoors || [], r => r.name, r => r.count, 'bg-danger');
}
function renderFiles(data) {
renderRows('file-download-rows', data.top_files || [], r => r.name, r => r.count, 'bg-success');
renderRows('filearea-rows', data.top_fileareas || [], r => r.area_name, r => r.count, 'bg-info');
}
function renderNodelist(data) {
renderRows('nodelist-search-rows', data.top_nodelist_searches || [], r => r.name, r => r.count, 'bg-secondary');
renderRows('node-view-rows', data.top_nodes || [], r => r.name, r => r.count, 'bg-primary');
}
function renderUsers(data) {
renderRows('top-users-rows', data.top_users || [], r => r.username || uiT('ui.admin.activity_stats.anonymous', '(anon)'), r => r.count, 'bg-primary');
}
function renderInterests(data) {
const tbody = document.getElementById('interest-rows');
if (!tbody) return;
const interests = data.popular_interests || [];
if (interests.length === 0) {
tbody.innerHTML = `<tr><td colspan="3" class="text-muted text-center py-3">${uiT('ui.admin.activity_stats.no_data_for_period', 'No data for this period')}</td></tr>`;
return;
}
const maxVal = Math.max(...interests.map(r => r.subscribers || 0));
tbody.innerHTML = interests.map(r => {
const val = r.subscribers || 0;
const icon = escapeHtml(r.icon || 'fa-layer-group');
const color = escapeHtml(r.color || '#6c757d');
const name = escapeHtml(r.name || '');
return `<tr>
<td class="text-truncate" style="max-width:220px">
<i class="fas ${icon} me-2" style="color:${color}"></i>${name}
</td>
<td class="text-end fw-semibold">${val.toLocaleString()}</td>
<td>${progressBar(val, maxVal, 'bg-primary')}</td>
</tr>`;
}).join('');
}
function renderHourly(data) {
const hourly = data.hourly || [];
const tbody = document.getElementById('hourly-rows');
const tzAbbr = getTzAbbr(data.timezone || userTimezone);
document.getElementById('hourly-tz-label').textContent = tzAbbr;
document.querySelectorAll('.hourly-tz-col').forEach(el => el.textContent = tzAbbr);
if (hourly.length === 0) {
tbody.innerHTML = `<tr><td colspan="3" class="text-muted text-center py-3">${uiT('ui.admin.activity_stats.no_data', 'No data')}</td></tr>`;
return;
}
const maxH = Math.max(...hourly.map(h => h.count || 0));
tbody.innerHTML = hourly.map(h => {
const label = String(h.hour).padStart(2, '0') + ':00';
return `<tr>
<td>${label}</td>
<td class="text-end fw-semibold">${(h.count || 0).toLocaleString()}</td>
<td>${progressBar(h.count || 0, maxH, 'bg-primary')}</td>
</tr>`;
}).join('');
}
function renderAll(data) {
renderSummaryCards(data);
renderOverview(data);
renderAreas(data);
renderDoors(data);
renderFiles(data);
renderNodelist(data);
renderUsers(data);
renderHourly(data);
renderInterests(data);
}
function loadStats() {
const period = document.getElementById('period-select').value;
const excludeAdmins = document.getElementById('exclude-admins').checked ? '1' : '0';
document.getElementById('loading-indicator').style.display = '';
document.getElementById('stats-content').style.display = 'none';
$.getJSON('/admin/api/activity-stats?period=' + encodeURIComponent(period) + '&exclude_admins=' + excludeAdmins + '&timezone=' + encodeURIComponent(userTimezone))
.done(function(data) {
renderAll(data);
document.getElementById('loading-indicator').style.display = 'none';
document.getElementById('stats-content').style.display = '';
})
.fail(function(xhr) {
document.getElementById('loading-indicator').innerHTML =
`<div class="alert alert-danger">${uiT('ui.admin.activity_stats.failed_load_statistics_prefix', 'Failed to load statistics. ')} ` +
escapeHtml(apiError(xhr.responseJSON, '')) + '</div>';
});
}
document.getElementById('period-select').addEventListener('change', loadStats);
document.getElementById('exclude-admins').addEventListener('change', loadStats);
$(document).ready(function() { loadI18nNamespaces(['common', 'errors']).then(function() { loadStats(); }); });
})();
</script>
{% endblock %}
|