pmtiles-swarm 0.45.1 → 0.46.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 +54 -0
- package/package.json +1 -1
- package/src/api.js +21 -2
- package/src/web/index.html +83 -25
- package/src/web/public.html +57 -19
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,60 @@
|
|
|
7
7
|
### 🐞 Bug fixes
|
|
8
8
|
- _...Add new stuff here..._
|
|
9
9
|
|
|
10
|
+
## 0.46.0
|
|
11
|
+
### ✨ Features and improvements
|
|
12
|
+
- **Each category endpoint on the public page is now offered the way it is used.** A `.torrent` is a
|
|
13
|
+
file, so it downloads. A preview is a page, so it opens. The rest are addresses that belong
|
|
14
|
+
somewhere else — a style, a torrent client, a feed reader — and following one here achieves
|
|
15
|
+
nothing, so TileJSON, magnet, RSS and the style URL copy instead.
|
|
16
|
+
|
|
17
|
+
The style URL is no longer printed. It is a TileJSON URL carrying a `.torrent` URL and a
|
|
18
|
+
percent-encoded magnet in its fragment, several hundred characters of it, and it was most of the
|
|
19
|
+
card — while being the one thing on the page nobody reads and everybody pastes. Every copy button
|
|
20
|
+
also carries its URL as a tooltip, since a browser on plain HTTP has no clipboard and a string
|
|
21
|
+
somebody can select beats a button that does nothing.
|
|
22
|
+
|
|
23
|
+
### 🐞 Bug fixes
|
|
24
|
+
|
|
25
|
+
## 0.45.4
|
|
26
|
+
### ✨ Features and improvements
|
|
27
|
+
|
|
28
|
+
### 🐞 Bug fixes
|
|
29
|
+
- **URLs shown in the console name the public port, not the admin one.** The console is served by the
|
|
30
|
+
admin listener, so every address it handed out named that listener — a TileJSON URL on `:8091`, a
|
|
31
|
+
`.torrent` on `:8091`, a style URL carrying both. Those are exactly the addresses that get pasted
|
|
32
|
+
into a style, a torrent client or another node, and none of them can reach the admin port: it binds
|
|
33
|
+
to localhost by default and serves nothing public even when it does not.
|
|
34
|
+
|
|
35
|
+
Only the port is corrected, and only when it is the admin one. The host stays whatever the request
|
|
36
|
+
arrived as, so a node behind a proxy still names itself correctly without anyone setting
|
|
37
|
+
`publicUrl` — and `publicUrl` still wins outright where it is set.
|
|
38
|
+
|
|
39
|
+
## 0.45.3
|
|
40
|
+
### ✨ Features and improvements
|
|
41
|
+
|
|
42
|
+
### 🐞 Bug fixes
|
|
43
|
+
- **The Copy and Open buttons on the Categories tab no longer sit off the edge of the page.** Table
|
|
44
|
+
cells are `nowrap`, which is right for the archive list where a wrapped number is worse than a wide
|
|
45
|
+
column — but a category cell holds a style URL and a paragraph explaining it, so nothing could
|
|
46
|
+
wrap and the table grew past the window, carrying the buttons at its end out of reach. The
|
|
47
|
+
endpoints table lays out fixed now: the label and button columns keep their width and the URL takes
|
|
48
|
+
what is left.
|
|
49
|
+
|
|
50
|
+
## 0.45.2
|
|
51
|
+
### ✨ Features and improvements
|
|
52
|
+
|
|
53
|
+
### 🐞 Bug fixes
|
|
54
|
+
- **A peer's pieces bar now gets a row of its own on the Peers tab.** As a column it took most of the
|
|
55
|
+
table and left every other cell wrapping a word at a time — `WebT orrent 0.3. 0` down four lines —
|
|
56
|
+
while the bar itself was still too narrow to read. The figures go on one line and the bar spans the
|
|
57
|
+
table beneath them, labelled **pieces** rather than **Has**.
|
|
58
|
+
- **A peer that describes itself with an object is no longer shown as `[object Object]`.** Engines
|
|
59
|
+
disagree about the shape: libtorrent sends a version string and a WebTorrent peer arrives as an
|
|
60
|
+
object, which went into the column unchanged — and that reads as a fault in this node rather than a
|
|
61
|
+
peer describing itself differently. Whatever the object carries that names it is used, and a dash
|
|
62
|
+
where nothing does.
|
|
63
|
+
|
|
10
64
|
## 0.45.1
|
|
11
65
|
### ✨ Features and improvements
|
|
12
66
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmtiles-swarm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.46.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/api.js
CHANGED
|
@@ -317,8 +317,27 @@ export function createApp({
|
|
|
317
317
|
// a torrent client handed the `torrent` link, another node syncing from
|
|
318
318
|
// the feed -- silently gets something it cannot use.
|
|
319
319
|
const configured = String(config.publicUrl ?? '').trim();
|
|
320
|
-
|
|
321
|
-
|
|
320
|
+
if (configured) return configured.replace(/\/$/, '');
|
|
321
|
+
|
|
322
|
+
// Never the admin port, whichever one the request came in on.
|
|
323
|
+
//
|
|
324
|
+
// The console lives on the admin listener, so every URL it showed named
|
|
325
|
+
// that listener — a TileJSON URL on :8091, a `.torrent` on :8091, a style
|
|
326
|
+
// URL carrying both. Those are the addresses people copy into a style, a
|
|
327
|
+
// torrent client or another node, and none of them can reach the admin
|
|
328
|
+
// port: it is bound to localhost by default and serves nothing public
|
|
329
|
+
// even when it is not. Half the addresses on a node pointing somewhere
|
|
330
|
+
// the other half do not is a confusion nobody should have to diagnose.
|
|
331
|
+
//
|
|
332
|
+
// The host is still whatever the request arrived as, so a node behind a
|
|
333
|
+
// proxy keeps naming itself correctly without anybody configuring
|
|
334
|
+
// `publicUrl`. Only the port is corrected, and only when it is the one
|
|
335
|
+
// port that is not for the public.
|
|
336
|
+
const host = onAdminPort(req)
|
|
337
|
+
? String(req.host).replace(/:\d+$/, '') +
|
|
338
|
+
(config.port ? `:${config.port}` : '')
|
|
339
|
+
: req.host;
|
|
340
|
+
return `${req.protocol}://${host}`.replace(/\/$/, '');
|
|
322
341
|
};
|
|
323
342
|
|
|
324
343
|
/**
|
package/src/web/index.html
CHANGED
|
@@ -224,6 +224,15 @@
|
|
|
224
224
|
}
|
|
225
225
|
.tag.on { background: var(--accent); color: #fff; border-color: var(--accent); }
|
|
226
226
|
#add-error { color: var(--bad); min-height: 1.2rem; }
|
|
227
|
+
/* Category endpoints. `th, td` is nowrap for the archive table, where a
|
|
228
|
+
wrapped number is worse than a wide one — but here the cell holds a
|
|
229
|
+
style URL and a paragraph explaining it, so nowrap pushed the table
|
|
230
|
+
wider than the window and took the Copy and Open buttons off the far
|
|
231
|
+
edge with it. Fixed layout keeps the two end columns where they are
|
|
232
|
+
and gives the middle whatever is left. */
|
|
233
|
+
#category-list table { table-layout: fixed; }
|
|
234
|
+
#category-list td { white-space: normal; }
|
|
235
|
+
#category-list code { word-break: break-all; }
|
|
227
236
|
/* Separates what acts on one archive from what acts on all of them. */
|
|
228
237
|
.bar-gap { flex: 1 1 1.5rem; min-width: 0.75rem; }
|
|
229
238
|
.tabs {
|
|
@@ -252,6 +261,12 @@
|
|
|
252
261
|
word-break: break-all;
|
|
253
262
|
}
|
|
254
263
|
.rows th { color: var(--muted); font-size: 0.75rem; white-space: nowrap; }
|
|
264
|
+
/* A peer's figures fit on one line once the bar is not competing with
|
|
265
|
+
them for width; the bar gets the whole table on the row below. */
|
|
266
|
+
.rows.peers td { white-space: nowrap; word-break: normal; }
|
|
267
|
+
.rows.peers tr.peerbar > td { border-bottom: 1px solid var(--line); }
|
|
268
|
+
.rows.peers tr.peerbar .piecerow { margin: 0; }
|
|
269
|
+
.rows.peers tr:not(.peerbar) > td { border-bottom: none; padding-bottom: 0.1rem; }
|
|
255
270
|
[hidden] { display: none !important; }
|
|
256
271
|
|
|
257
272
|
/* A radio or checkbox beside its own label.
|
|
@@ -1588,6 +1603,30 @@
|
|
|
1588
1603
|
* @param {object} entry - What the detail endpoint already returned.
|
|
1589
1604
|
* @returns {Promise<void>} - Resolves once rendered.
|
|
1590
1605
|
*/
|
|
1606
|
+
/**
|
|
1607
|
+
* What a peer says it is, as a string.
|
|
1608
|
+
*
|
|
1609
|
+
* Engines disagree about the shape: libtorrent sends a version string,
|
|
1610
|
+
* and a WebTorrent peer arrives as an object. The object went through
|
|
1611
|
+
* escapeHtml unchanged and the column read "[object Object]" — which
|
|
1612
|
+
* looks like a fault in this node rather than a peer that described
|
|
1613
|
+
* itself differently.
|
|
1614
|
+
* @param {object} peer - One peer from the engine.
|
|
1615
|
+
* @returns {string} - Something worth printing.
|
|
1616
|
+
*/
|
|
1617
|
+
function clientOf(peer) {
|
|
1618
|
+
const said = peer?.client;
|
|
1619
|
+
if (typeof said === 'string' && said.trim()) return said;
|
|
1620
|
+
if (said && typeof said === 'object') {
|
|
1621
|
+
// Whatever it does carry, in the order most likely to name it.
|
|
1622
|
+
for (const key of ['name', 'client', 'version', 'id', 'peerId']) {
|
|
1623
|
+
const value = said[key];
|
|
1624
|
+
if (typeof value === 'string' && value.trim()) return value;
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
return '—';
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1591
1630
|
async function fillPane(name, infoHash, entry) {
|
|
1592
1631
|
const pane = document.querySelector(`[data-pane="${name}"]`);
|
|
1593
1632
|
if (!pane) return;
|
|
@@ -1677,39 +1716,58 @@
|
|
|
1677
1716
|
(peer) => !listed.has(peer.address),
|
|
1678
1717
|
);
|
|
1679
1718
|
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
have
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1719
|
+
// Two rows a peer: the figures, then its pieces across the full
|
|
1720
|
+
// width. As a column the bar took most of the table and left every
|
|
1721
|
+
// other cell wrapping a word at a time -- "WebT orrent 0.3. 0" in
|
|
1722
|
+
// four lines -- while the bar itself was still too narrow to read.
|
|
1723
|
+
const columns = ['Address', 'Client', 'Kind', 'Conn', 'Progress', 'Down', 'Up'];
|
|
1724
|
+
const body = [...peers, ...extra]
|
|
1725
|
+
.map((peer, index) => {
|
|
1726
|
+
const address = peer.address ?? peer.ip ?? '—';
|
|
1727
|
+
const have = bitfields.get(address);
|
|
1728
|
+
const cells = [
|
|
1729
|
+
escapeHtml(address),
|
|
1730
|
+
escapeHtml(clientOf(peer)),
|
|
1731
|
+
escapeHtml(peer.kind ?? 'peer'),
|
|
1732
|
+
escapeHtml(peer.connection ?? '—'),
|
|
1733
|
+
peer.progress != null ? pct(peer.progress) : '—',
|
|
1734
|
+
rate(peer.downloadSpeed ?? peer.dlSpeed),
|
|
1735
|
+
rate(peer.uploadSpeed ?? peer.upSpeed),
|
|
1736
|
+
];
|
|
1737
|
+
return (
|
|
1738
|
+
`<tr>${cells.map((cell) => `<td>${cell}</td>`).join('')}</tr>` +
|
|
1739
|
+
`<tr class="peerbar"><td colspan="${columns.length}">
|
|
1740
|
+
<div class="piecerow">
|
|
1741
|
+
<span class="label">pieces</span>
|
|
1742
|
+
${
|
|
1743
|
+
have
|
|
1744
|
+
? `<canvas class="piecebar thin" data-bar="peer-${index}"></canvas>`
|
|
1745
|
+
: '<span class="sub">not reported</span>'
|
|
1746
|
+
}
|
|
1747
|
+
<span class="value">${peer.progress != null ? pct(peer.progress) : ''}</span>
|
|
1748
|
+
</div>
|
|
1749
|
+
</td></tr>`
|
|
1750
|
+
);
|
|
1751
|
+
})
|
|
1752
|
+
.join('');
|
|
1696
1753
|
|
|
1697
1754
|
pane.innerHTML =
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
rows
|
|
1701
|
-
|
|
1755
|
+
(body === ''
|
|
1756
|
+
? '<div class="sub">Nothing to show.</div>'
|
|
1757
|
+
: `<table class="rows peers"><thead><tr>${columns
|
|
1758
|
+
.map((header) => `<th>${escapeHtml(header)}</th>`)
|
|
1759
|
+
.join('')}</tr></thead><tbody>${body}</tbody></table>`) +
|
|
1702
1760
|
(failed
|
|
1703
1761
|
? `<div class="sub" style="margin-top:0.5rem">
|
|
1704
1762
|
The engine could not list peers: ${escapeHtml(failed)}
|
|
1705
1763
|
</div>`
|
|
1706
1764
|
: '') +
|
|
1707
|
-
(
|
|
1765
|
+
(body !== ''
|
|
1708
1766
|
? `<div class="sub" style="margin-top:0.5rem">
|
|
1709
|
-
<b>
|
|
1710
|
-
a column covers many pieces and is only
|
|
1711
|
-
them are held. <b>Progress</b> is the
|
|
1712
|
-
for the same peer, which is exact.
|
|
1767
|
+
Each peer's <b>pieces</b> bar is drawn the way the bars on
|
|
1768
|
+
General are — a column covers many pieces and is only
|
|
1769
|
+
filled when all of them are held. <b>Progress</b> is the
|
|
1770
|
+
engine's own figure for the same peer, which is exact.
|
|
1713
1771
|
</div>`
|
|
1714
1772
|
: '');
|
|
1715
1773
|
|
package/src/web/public.html
CHANGED
|
@@ -118,6 +118,19 @@
|
|
|
118
118
|
gap: 0.35rem 0.9rem;
|
|
119
119
|
font-size: 0.9rem;
|
|
120
120
|
}
|
|
121
|
+
/* A copy button reads as a link's sibling, not as a form control. */
|
|
122
|
+
.copy {
|
|
123
|
+
border: 1px solid var(--line);
|
|
124
|
+
border-radius: 999px;
|
|
125
|
+
background: none;
|
|
126
|
+
color: var(--muted);
|
|
127
|
+
font: inherit;
|
|
128
|
+
font-size: 0.74rem;
|
|
129
|
+
padding: 0 0.45rem;
|
|
130
|
+
cursor: pointer;
|
|
131
|
+
}
|
|
132
|
+
.copy:hover { color: var(--fg); border-color: var(--fg); }
|
|
133
|
+
.copy.done { border-color: currentColor; color: #3fb950; }
|
|
121
134
|
.urls {
|
|
122
135
|
margin-top: 0.7rem;
|
|
123
136
|
display: grid;
|
|
@@ -390,30 +403,55 @@
|
|
|
390
403
|
a.href = href;
|
|
391
404
|
links.append(a);
|
|
392
405
|
};
|
|
406
|
+
// What each endpoint is for decides how it is offered. A .torrent
|
|
407
|
+
// is a file to fetch, so it stays a link the browser downloads; a
|
|
408
|
+
// preview is a page to visit. The rest are addresses that belong
|
|
409
|
+
// somewhere else — a style, a torrent client, a feed reader — and
|
|
410
|
+
// following one here achieves nothing, so those copy.
|
|
411
|
+
const copy = (href, text) => {
|
|
412
|
+
const button = el('button', 'copy', text);
|
|
413
|
+
button.type = 'button';
|
|
414
|
+
const absolute = new URL(href, location.href).href;
|
|
415
|
+
// The URL either way: a browser on plain HTTP has no clipboard,
|
|
416
|
+
// and a tooltip somebody can select beats a button that does
|
|
417
|
+
// nothing.
|
|
418
|
+
button.title = absolute;
|
|
419
|
+
button.onclick = async () => {
|
|
420
|
+
try {
|
|
421
|
+
await navigator.clipboard.writeText(absolute);
|
|
422
|
+
button.classList.add('done');
|
|
423
|
+
button.textContent = 'copied';
|
|
424
|
+
setTimeout(() => {
|
|
425
|
+
button.classList.remove('done');
|
|
426
|
+
button.textContent = text;
|
|
427
|
+
}, 1200);
|
|
428
|
+
} catch {
|
|
429
|
+
button.textContent = 'copy failed — it is in the tooltip';
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
links.append(button);
|
|
433
|
+
};
|
|
434
|
+
|
|
393
435
|
const ends = entry.endpoints;
|
|
394
|
-
if (ends.tileJson)
|
|
436
|
+
if (ends.tileJson) copy(ends.tileJson, 'TileJSON');
|
|
395
437
|
// The newest build's preview, which is what "show me this
|
|
396
438
|
// category" means.
|
|
397
439
|
if (ends.preview) add(ends.preview, 'preview');
|
|
398
|
-
|
|
399
|
-
if (ends.
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
// The one worth copying: the TileJSON URL with the .torrent URL and
|
|
405
|
-
// the magnet in its fragment, which a plain client fetches over HTTP
|
|
406
|
-
// and a swarm-aware one joins directly.
|
|
407
|
-
if (ends.styleUrl) {
|
|
408
|
-
urls.append(
|
|
409
|
-
urlRow('style URL', new URL(ends.styleUrl, location.href).href),
|
|
410
|
-
);
|
|
411
|
-
} else if (ends.tileJson) {
|
|
412
|
-
urls.append(
|
|
413
|
-
urlRow('TileJSON', new URL(ends.tileJson, location.href).href),
|
|
414
|
-
);
|
|
440
|
+
// A file, so a link the browser saves rather than renders.
|
|
441
|
+
if (ends.torrent) {
|
|
442
|
+
const a = el('a', null, '.torrent');
|
|
443
|
+
a.href = ends.torrent;
|
|
444
|
+
a.download = '';
|
|
445
|
+
links.append(a);
|
|
415
446
|
}
|
|
416
|
-
if (
|
|
447
|
+
if (ends.magnet) copy(ends.magnet, 'magnet');
|
|
448
|
+
if (ends.feed) copy(ends.feed, 'RSS');
|
|
449
|
+
// The one worth copying and the one nobody wants to read: a TileJSON
|
|
450
|
+
// URL carrying the .torrent URL and a percent-encoded magnet in its
|
|
451
|
+
// fragment. Printed in full it was most of the card — several
|
|
452
|
+
// hundred characters of it — and it is never read, only pasted.
|
|
453
|
+
if (ends.styleUrl) copy(ends.styleUrl, 'style URL');
|
|
454
|
+
card.append(links);
|
|
417
455
|
|
|
418
456
|
host.append(card);
|
|
419
457
|
}
|