pmtiles-swarm 0.15.2 β 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/package.json +1 -1
- package/src/web/index.html +165 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,22 @@
|
|
|
7
7
|
### π Bug fixes
|
|
8
8
|
- _...Add new stuff here..._
|
|
9
9
|
|
|
10
|
+
## 0.16.0
|
|
11
|
+
### β¨ Features and improvements
|
|
12
|
+
- **A Traffic tab in the console**, which is where the statistics added in 0.10.0 should have been
|
|
13
|
+
all along β until now the only thing on screen was a single "tiles served" figure on an
|
|
14
|
+
archive's detail, and the node-level report, the breakdowns and the recent requests had no user
|
|
15
|
+
interface at all.
|
|
16
|
+
|
|
17
|
+
It shows what this node has served: totals, then per archive with a zoom histogram, a status
|
|
18
|
+
breakdown, p50/p95 and how many distinct clients β and the last forty requests with address,
|
|
19
|
+
tile, status, size and duration. Refreshes every five seconds while the tab is open, with a
|
|
20
|
+
switch to stop it, and a reset button that is deliberately a separate action from reading, so a
|
|
21
|
+
page polling the endpoint can never erase the history it is drawing.
|
|
22
|
+
|
|
23
|
+
Behind a load balancer this is the only honest way to see how traffic is really distributed:
|
|
24
|
+
open it on each node and compare, rather than trusting what the balancer believes it sent.
|
|
25
|
+
|
|
10
26
|
## 0.15.2
|
|
11
27
|
### π Bug fixes
|
|
12
28
|
- **A mutable magnet is named after its category rather than a build.** `dn=` was taken from
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmtiles-swarm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "BitTorrent distribution for PMTiles map archives: create torrents, watch folders, publish and subscribe to RSS feeds, and seed through qBittorrent or an embedded client",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
package/src/web/index.html
CHANGED
|
@@ -309,6 +309,7 @@
|
|
|
309
309
|
<nav>
|
|
310
310
|
<button id="tab-archives" class="on">Archives</button>
|
|
311
311
|
<button id="tab-categories">Categories</button>
|
|
312
|
+
<button id="tab-traffic">Traffic</button>
|
|
312
313
|
<button id="tab-settings">Settings</button>
|
|
313
314
|
<button id="logout" hidden>Sign out</button>
|
|
314
315
|
</nav>
|
|
@@ -366,6 +367,17 @@
|
|
|
366
367
|
</div>
|
|
367
368
|
</section>
|
|
368
369
|
|
|
370
|
+
<section id="view-traffic" hidden>
|
|
371
|
+
<div class="bar">
|
|
372
|
+
<button id="traffic-refresh">Refresh</button>
|
|
373
|
+
<label class="choice">
|
|
374
|
+
<input type="checkbox" id="traffic-live" checked /> keep refreshing
|
|
375
|
+
</label>
|
|
376
|
+
<button id="traffic-reset" style="margin-left:auto">Reset counters</button>
|
|
377
|
+
</div>
|
|
378
|
+
<div id="traffic-body"><div class="sub">loadingβ¦</div></div>
|
|
379
|
+
</section>
|
|
380
|
+
|
|
369
381
|
<section id="view-settings" hidden>
|
|
370
382
|
<div class="bar">
|
|
371
383
|
<button id="settings-save">Save changes</button>
|
|
@@ -3888,12 +3900,13 @@
|
|
|
3888
3900
|
|
|
3889
3901
|
// ββ Tabs ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
3890
3902
|
const showTab = (name) => {
|
|
3891
|
-
for (const tab of ['archives', 'categories', 'settings']) {
|
|
3903
|
+
for (const tab of ['archives', 'categories', 'traffic', 'settings']) {
|
|
3892
3904
|
$(`view-${tab}`).hidden = tab !== name;
|
|
3893
3905
|
$(`tab-${tab}`).classList.toggle('on', tab === name);
|
|
3894
3906
|
}
|
|
3895
3907
|
if (name === 'settings') loadSettings().catch((e) => toast(e.message));
|
|
3896
3908
|
if (name === 'categories') loadCategories().catch((e) => toast(e.message));
|
|
3909
|
+
if (name === 'traffic') loadTraffic().catch((e) => toast(e.message));
|
|
3897
3910
|
};
|
|
3898
3911
|
// The footer's year, set from the clock rather than typed into a file
|
|
3899
3912
|
// nobody will remember to edit. The version beside it arrives with the
|
|
@@ -3902,6 +3915,157 @@
|
|
|
3902
3915
|
|
|
3903
3916
|
$('tab-archives').onclick = () => showTab('archives');
|
|
3904
3917
|
$('tab-categories').onclick = () => showTab('categories');
|
|
3918
|
+
$('tab-traffic').onclick = () => showTab('traffic');
|
|
3919
|
+
|
|
3920
|
+
// ββ Traffic βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
3921
|
+
//
|
|
3922
|
+
// What this node has served, which is a different question from whether
|
|
3923
|
+
// it is healthy. Behind a balancer it is the only honest way to see how
|
|
3924
|
+
// traffic is really distributed: ask each node what it did, rather than
|
|
3925
|
+
// trusting what the balancer believes it sent.
|
|
3926
|
+
let trafficTimer = null;
|
|
3927
|
+
|
|
3928
|
+
/** Draws a zoom histogram, since zoom is the shape of the load. */
|
|
3929
|
+
const zoomBars = (counts) => {
|
|
3930
|
+
const entries = Object.entries(counts ?? {});
|
|
3931
|
+
if (entries.length === 0) return '<span class="sub">β</span>';
|
|
3932
|
+
const peak = Math.max(...entries.map(([, n]) => n));
|
|
3933
|
+
return entries
|
|
3934
|
+
.map(([z, n]) => {
|
|
3935
|
+
const height = Math.max(2, Math.round((n / peak) * 20));
|
|
3936
|
+
return `<span title="z${z}: ${n.toLocaleString()}" style="display:inline-block;width:6px;margin-right:1px;vertical-align:bottom;height:${height}px;background:var(--accent);opacity:0.75"></span>`;
|
|
3937
|
+
})
|
|
3938
|
+
.join('');
|
|
3939
|
+
};
|
|
3940
|
+
|
|
3941
|
+
const statusList = (counts) => {
|
|
3942
|
+
const entries = Object.entries(counts ?? {});
|
|
3943
|
+
if (entries.length === 0) return '<span class="sub">β</span>';
|
|
3944
|
+
return entries.map(([code, n]) => `${code} ${n.toLocaleString()}`).join(' Β· ');
|
|
3945
|
+
};
|
|
3946
|
+
|
|
3947
|
+
/**
|
|
3948
|
+
* Reads the report and draws it.
|
|
3949
|
+
* @returns {Promise<void>} - Resolves once drawn.
|
|
3950
|
+
*/
|
|
3951
|
+
async function loadTraffic() {
|
|
3952
|
+
const body = $('traffic-body');
|
|
3953
|
+
let report;
|
|
3954
|
+
try {
|
|
3955
|
+
report = await api('/api/stats?recent=40');
|
|
3956
|
+
} catch (error) {
|
|
3957
|
+
if (error.status === 501) {
|
|
3958
|
+
body.innerHTML =
|
|
3959
|
+
'<div class="panel"><div class="sub">Tile statistics are turned off on this node β set <code>tileStats</code> in the configuration to enable them.</div></div>';
|
|
3960
|
+
return;
|
|
3961
|
+
}
|
|
3962
|
+
throw error;
|
|
3963
|
+
}
|
|
3964
|
+
|
|
3965
|
+
const archives = Object.entries(report.archives ?? {}).sort(
|
|
3966
|
+
(a, b) => b[1].requests - a[1].requests,
|
|
3967
|
+
);
|
|
3968
|
+
const since = new Date(report.since);
|
|
3969
|
+
const recent = report.recent ?? [];
|
|
3970
|
+
|
|
3971
|
+
const archiveRows = archives
|
|
3972
|
+
.map(([hash, a]) => {
|
|
3973
|
+
const clients = Object.keys(a.clients ?? {}).length;
|
|
3974
|
+
return `<tr>
|
|
3975
|
+
<td><b>${escapeHtml(a.name ?? hash.slice(0, 12))}</b>
|
|
3976
|
+
<div class="sub">${escapeHtml(hash.slice(0, 12))}β¦${clients ? ` Β· ${clients} client${clients === 1 ? '' : 's'}` : ''}</div></td>
|
|
3977
|
+
<td>${a.requests.toLocaleString()}</td>
|
|
3978
|
+
<td>${bytes(a.bytes)}</td>
|
|
3979
|
+
<td>${zoomBars(a.byZoom)}</td>
|
|
3980
|
+
<td class="sub">${statusList(a.byStatus)}</td>
|
|
3981
|
+
<td>${a.p50ms === null ? 'β' : `${Math.round(a.p50ms)} / ${Math.round(a.p95ms)} ms`}</td>
|
|
3982
|
+
</tr>`;
|
|
3983
|
+
})
|
|
3984
|
+
.join('');
|
|
3985
|
+
|
|
3986
|
+
const recentRows = recent
|
|
3987
|
+
.map(
|
|
3988
|
+
(row) => `<tr>
|
|
3989
|
+
<td class="sub">${new Date(row.at).toLocaleTimeString()}</td>
|
|
3990
|
+
<td>${escapeHtml(row.ip ?? 'β')}</td>
|
|
3991
|
+
<td class="sub">${escapeHtml(row.name ?? (row.infoHash ?? '').slice(0, 12) ?? 'β')}</td>
|
|
3992
|
+
<td>${row.z ?? 'β'}/${row.x ?? 'β'}/${row.y ?? 'β'}</td>
|
|
3993
|
+
<td>${row.status ?? 'β'}</td>
|
|
3994
|
+
<td>${bytes(row.bytes)}</td>
|
|
3995
|
+
<td>${row.ms === null ? 'β' : `${Math.round(row.ms)} ms`}</td>
|
|
3996
|
+
</tr>`,
|
|
3997
|
+
)
|
|
3998
|
+
.join('');
|
|
3999
|
+
|
|
4000
|
+
body.innerHTML = `
|
|
4001
|
+
<div class="panel" style="margin-bottom:0.8rem">
|
|
4002
|
+
<h2 style="margin-top:0">${escapeHtml(report.node ?? 'this node')}</h2>
|
|
4003
|
+
<div class="grid">
|
|
4004
|
+
<div><span>requests</span><b>${(report.requests ?? 0).toLocaleString()}</b></div>
|
|
4005
|
+
<div><span>sent</span><b>${bytes(report.bytes)}</b></div>
|
|
4006
|
+
<div><span>archives touched</span><b>${archives.length}</b></div>
|
|
4007
|
+
<div><span>counting since</span><b>${since.toLocaleTimeString()}</b>
|
|
4008
|
+
<span class="sub">a restart clears this</span></div>
|
|
4009
|
+
</div>
|
|
4010
|
+
</div>
|
|
4011
|
+
|
|
4012
|
+
${
|
|
4013
|
+
archives.length === 0
|
|
4014
|
+
? '<div class="panel"><div class="sub">Nothing served yet. Counters start empty and a restart clears them.</div></div>'
|
|
4015
|
+
: `<div class="panel" style="margin-bottom:0.8rem">
|
|
4016
|
+
<h2 style="margin-top:0">By archive</h2>
|
|
4017
|
+
<table><thead><tr>
|
|
4018
|
+
<th>Archive</th><th>Requests</th><th>Sent</th><th>Zooms</th><th>Status</th><th>p50 / p95</th>
|
|
4019
|
+
</tr></thead><tbody>${archiveRows}</tbody></table>
|
|
4020
|
+
<div class="sub" style="margin-top:0.5rem">
|
|
4021
|
+
Bytes are counted as sent, so a gzipped vector tile counts compressed β the
|
|
4022
|
+
number that matters for bandwidth, and not what the client ends up holding.
|
|
4023
|
+
</div>
|
|
4024
|
+
</div>`
|
|
4025
|
+
}
|
|
4026
|
+
|
|
4027
|
+
${
|
|
4028
|
+
recent.length === 0
|
|
4029
|
+
? ''
|
|
4030
|
+
: `<div class="panel">
|
|
4031
|
+
<h2 style="margin-top:0">Recent requests</h2>
|
|
4032
|
+
<table><thead><tr>
|
|
4033
|
+
<th>When</th><th>From</th><th>Archive</th><th>Tile</th><th>Status</th><th>Size</th><th>Took</th>
|
|
4034
|
+
</tr></thead><tbody>${recentRows}</tbody></table>
|
|
4035
|
+
<div class="sub" style="margin-top:0.5rem">
|
|
4036
|
+
The address is whatever this process can see. Behind a proxy sending no
|
|
4037
|
+
<code>X-Forwarded-For</code> it is the proxy for everything arriving through
|
|
4038
|
+
it β which still separates direct traffic from proxied, but not who sent it.
|
|
4039
|
+
</div>
|
|
4040
|
+
</div>`
|
|
4041
|
+
}`;
|
|
4042
|
+
}
|
|
4043
|
+
|
|
4044
|
+
const trafficPoll = () => {
|
|
4045
|
+
clearInterval(trafficTimer);
|
|
4046
|
+
if (!$('traffic-live').checked) return;
|
|
4047
|
+
trafficTimer = setInterval(() => {
|
|
4048
|
+
if (!$('view-traffic').hidden) loadTraffic().catch(() => {});
|
|
4049
|
+
}, 5000);
|
|
4050
|
+
};
|
|
4051
|
+
|
|
4052
|
+
$('traffic-refresh').onclick = () => loadTraffic().catch((e) => toast(e.message));
|
|
4053
|
+
$('traffic-live').onchange = trafficPoll;
|
|
4054
|
+
trafficPoll();
|
|
4055
|
+
|
|
4056
|
+
$('traffic-reset').onclick = async () => {
|
|
4057
|
+
// Deliberately separate from reading: a page polling the endpoint must
|
|
4058
|
+
// never be able to erase the history it is drawing.
|
|
4059
|
+
if (!confirm('Clear the counters and recent requests on this node?')) return;
|
|
4060
|
+
try {
|
|
4061
|
+
await api('/api/stats', { method: 'DELETE' });
|
|
4062
|
+
toast('Counters cleared');
|
|
4063
|
+
await loadTraffic();
|
|
4064
|
+
} catch (error) {
|
|
4065
|
+
toast(error.message);
|
|
4066
|
+
}
|
|
4067
|
+
};
|
|
4068
|
+
|
|
3905
4069
|
$('tab-settings').onclick = () => showTab('settings');
|
|
3906
4070
|
|
|
3907
4071
|
// ββ Sign in βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|