feat: ACP chart to show ap relay analytics

This commit is contained in:
Julian Lam
2026-04-16 11:43:10 -04:00
parent b8216c3f6a
commit 02ef509e5d
3 changed files with 198 additions and 40 deletions

View File

@@ -3,9 +3,53 @@
import { post, del } from 'api';
import { error } from 'alerts';
import { render } from 'benchpress';
import { get } from 'api';
import { translate } from 'translator';
import {
Chart,
LineController,
CategoryScale,
LinearScale,
LineElement,
PointElement,
Tooltip,
Filler,
Legend,
} from 'chart.js';
export function init() {
Chart.register(
LineController,
CategoryScale,
LinearScale,
LineElement,
PointElement,
Tooltip,
Filler,
Legend
);
let chart;
const labels = new Map([
['hourly', utils.getHoursArray().map(function (text, idx) {
return idx % 3 ? '' : text;
})],
['daily', utils.getDaysArray().map(function (text, idx) {
return idx % 3 ? '' : text;
})],
]);
export async function init() {
setupRelays();
chart = await initializeCharts();
const hostFilterEl = document.getElementById('hostFilter');
const termEl = document.getElementById('term');
if (hostFilterEl) {
hostFilterEl.addEventListener('change', updateCharts);
}
if (termEl) {
termEl.addEventListener('change', updateCharts);
}
};
function setupRelays() {
@@ -70,4 +114,79 @@ function throwModal() {
modal.find('input').focus();
});
});
}
}
async function updateCharts() {
const hostFilterEl = document.getElementById('hostFilter');
const termEl = document.getElementById('term');
console.log(hostFilterEl.value, termEl.value);
const data = await get(`/api${ajaxify.data.url}?host=${hostFilterEl.value}&term=${termEl.value}`);
chart.data.labels = labels.get(termEl.value || 'hourly');
chart.data.datasets[0].data = data.data.in;
chart.data.datasets[1].data = data.data.out;
chart.update();
}
async function initializeCharts() {
const canvas = document.querySelector('canvas');
if (utils.isMobile()) {
Chart.defaults.plugins.tooltip.enabled = false;
}
const commonDataSetOpts = {
label: '',
fill: true,
tension: 0.25,
pointHoverBackgroundColor: '#fff',
pointBorderColor: '#fff',
};
const data = {
labels: labels.get('hourly'),
datasets: [
{
...commonDataSetOpts,
label: await translate('[[admin/settings/activitypub:analytics.in]]'),
backgroundColor: 'rgba(161,181,108,0.2)',
borderColor: 'rgba(161,181,108,1)',
pointBackgroundColor: 'rgba(161,181,108,1)',
pointHoverBorderColor: 'rgba(161,181,108,1)',
data: ajaxify.data.data.in,
},
{
...commonDataSetOpts,
label: await translate('[[admin/settings/activitypub:analytics.out]]'),
backgroundColor: 'rgba(151,187,205,0.2)',
borderColor: 'rgba(151,187,205,1)',
pointBackgroundColor: 'rgba(151,187,205,1)',
pointHoverBorderColor: 'rgba(151,187,205,1)',
data: ajaxify.data.data.out,
},
],
};
canvas.width = $(canvas).parent().width();
const chartOpts = {
responsive: true,
animation: false,
scales: {
y: {
beginAtZero: true,
},
},
plugins: {
legend: {
position: 'bottom',
},
},
};
return new Chart(canvas.getContext('2d'), {
type: 'line',
data,
options: chartOpts,
});
}

View File

@@ -32,11 +32,27 @@ federationController.rules = async function (req, res) {
federationController.relays = async function (req, res) {
const relays = await activitypub.relays.list();
const urls = relays.map(({ url }) => url);
let { host, term } = req.query;
if (!urls.includes(host)) {
host = undefined;
}
let method = 'getHourlyStatsForSet';
let count = 24;
if (term === 'daily') {
method = 'getDailyStatsForSet';
count = 30;
}
const inSet = host ? `ap.relayIn:byHost:${host}` : 'ap.relayIn';
const outSet = host ? `ap.relayOut:byHost:${host}` : 'ap.relayOut';
const incoming = await analytics[method](inSet, Date.now(), count);
const out = await analytics[method](outSet, Date.now(), count);
res.render(`admin/federation/relays`, {
title: '[[admin/menu:federation/relays]]',
relays,
hideSave: true,
data: { in: incoming, out },
});
};

View File

@@ -1,39 +1,62 @@
<div class="acp-page-container">
<!-- IMPORT admin/partials/settings/header.tpl -->
<div class="row settings m-0">
<div id="spy-container" class="col-12 col-md-8 px-0 mb-4" tabindex="0">
<div id="relays" class="mb-4">
<p class="lead">[[admin/settings/activitypub:relays.intro]]</p>
<p class="text-warning">[[admin/settings/activitypub:relays.warning]]</p>
<div class="mb-3 table-responsive-md">
<table class="table table-striped" id="relays">
<thead>
<th>[[admin/settings/activitypub:relays.relay]]</th>
<th>[[admin/settings/activitypub:relays.state]]</th>
<th></th>
</thead>
<tbody>
{{{ each relays }}}
<tr data-url="{./url}">
<td>{./url}</td>
<td>{./label}</td>
<td><a href="#" data-action="relays.remove"><i class="fa fa-trash link-danger"></i></a></td>
</tr>
{{{ end }}}
</tbody>
<tfoot>
<tr>
<td colspan="3">
<button class="btn btn-sm btn-primary" data-action="relays.add">[[admin/settings/activitypub:relays.add]]</button>
</td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
<!-- IMPORT admin/partials/settings/toc.tpl -->
<div component="settings/main/header" class="row border-bottom py-2 m-0 mb-3 sticky-top acp-page-main-header align-items-center">
<div class="col-12 col-md-8 px-0 mb-1 mb-md-0">
<h4 class="fw-bold tracking-tight mb-0">{title}</h4>
</div>
</div>
<div class="row flex-column-reverse flex-md-row">
<div id="relays" class="col-12 col-md-4">
<p>[[admin/settings/activitypub:relays.intro]]</p>
<p class="text-warning">[[admin/settings/activitypub:relays.warning]]</p>
<div class="mb-3 table-responsive-md">
<table class="table table-striped" id="relays">
<thead>
<th>[[admin/settings/activitypub:relays.relay]]</th>
<th>[[admin/settings/activitypub:relays.state]]</th>
<th></th>
</thead>
<tbody>
{{{ each relays }}}
<tr data-url="{./url}">
<td>{./url}</td>
<td>{./label}</td>
<td><a href="#" data-action="relays.remove"><i class="fa fa-trash link-danger"></i></a></td>
</tr>
{{{ end }}}
</tbody>
<tfoot>
<tr>
<td colspan="3">
<button class="btn btn-sm btn-primary" data-action="relays.add">[[admin/settings/activitypub:relays.add]]</button>
</td>
</tr>
</tfoot>
</table>
</div>
</div>
<div class="col-12 col-md-8">
<div class="card">
<div class="card-body">
<div class="mb-3 row">
<div class="col-6">
<label class="form-label" for="hostFilter">[[admin/settings/activitypub:analytics.by-hostname]] ({relays.length})</label>
<select class="form-select" autocomplete="off" id="hostFilter">
<option value="">All relays</option>
{{{ each relays }}}
<option value="{./url}">{./url}</option>
{{{ end }}}
</select>
</div>
<div class="col-6">
<label class="form-label" for="term">[[admin/settings/activitypub:analytics.term]]</label>
<select class="form-select" autocomplete="off" id="term">
<option value="hourly">[[admin/settings/activitypub:analytics.hourly]]</option>
<option value="daily">[[admin/settings/activitypub:analytics.daily]]</option>
</select>
</div>
</div>
<canvas height="350"></canvas>
</div>
</div>
</div>
</div>