pmtiles-swarm 0.45.2 → 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 CHANGED
@@ -7,6 +7,46 @@
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
+
10
50
  ## 0.45.2
11
51
  ### ✨ Features and improvements
12
52
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmtiles-swarm",
3
- "version": "0.45.2",
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
- const base = configured || `${req.protocol}://${req.host}`;
321
- return base.replace(/\/$/, '');
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
  /**
@@ -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 {
@@ -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) add(ends.tileJson, '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
- if (ends.torrent) add(ends.torrent, '.torrent');
399
- if (ends.magnet) add(ends.magnet, 'magnet');
400
- if (ends.feed) add(ends.feed, 'RSS');
401
- card.append(links);
402
-
403
- const urls = el('div', 'urls');
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 (urls.childElementCount > 0) card.append(urls);
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
  }