pmtiles-swarm 0.4.5 → 0.4.6

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,24 @@
7
7
  ### 🐞 Bug fixes
8
8
  - _...Add new stuff here..._
9
9
 
10
+ ## 0.4.6
11
+ ### ✨ Features and improvements
12
+ - **A feed's categories are a list, and are called categories.** The console offered a single
13
+ "Tag as" string while the configuration has always accepted `categories` as a list — one
14
+ concept under two names across two editors. It is now the same Categories column a watched
15
+ folder has. An existing `"category": "openmaptiles"` still works and needs no migration.
16
+ Everything user-facing now says category rather than tag, including the `%G` placeholder's
17
+ description and the API table.
18
+
19
+ ### 🐞 Bug fixes
20
+ - **Saving in the console no longer deletes settings it does not show.** Each record was rebuilt
21
+ from the rendered columns alone, so every field without a column was dropped the first time
22
+ anyone pressed Save — a watched folder's `pieceLength`, `stabilitySeconds`, `trackers` and
23
+ `sparse`, a subscription's `savePath`. Nothing warned, because from the console's side the
24
+ save succeeded. Each row now remembers the entry it was rendered from and a save starts from
25
+ that, overlaying the columns; an emptied box still removes its field, since that is an
26
+ instruction rather than a gap.
27
+
10
28
  ## 0.4.5
11
29
  ### 🐞 Bug fixes
12
30
  - **A hook whose command could not be started is tried again.** Completion is recorded before
package/README.md CHANGED
@@ -587,7 +587,7 @@ matters there is `maxConnections`, since every peer holds a NAT table entry. See
587
587
  | `GET` | `/api/torrents/:infoHash/peers`, `/trackers`, `/content` | Per-peer, per-tracker and per-file detail |
588
588
  | `GET` | `/api/torrents/:infoHash/pieces` | Which pieces are held, how rare each is, and what peers hold |
589
589
  | `PATCH` | `/api/torrents/:infoHash/mode` | Switch between mirror and cache |
590
- | `PATCH` | `/api/torrents/:infoHash/categories` | Set, add or remove tags |
590
+ | `PATCH` | `/api/torrents/:infoHash/categories` | Set, add or remove categories |
591
591
  | `PATCH` | `/api/torrents/:infoHash/seeding` | Per-archive seeding limit, or "use the global one" |
592
592
  | `PATCH` `GET` | `/api/torrents/:infoHash/location` | Move the data; poll the move |
593
593
  | `POST` | `/api/torrents/:infoHash/pause`, `/resume` | Stop offering it, without forgetting it |
@@ -599,7 +599,7 @@ matters there is `maxConnections`, since every peer holds a NAT table entry. See
599
599
  | `POST` | `/api/check-origins` | Check every archive with a watchable source |
600
600
  | `GET` `DELETE` | `/api/adds` | Downloads still in flight, and cancelling one by URL |
601
601
  | `GET` `POST` | `/api/speed` | Which speed limits are in force, and the manual switch between the two sets |
602
- | `GET` | `/api/categories` | Every tag, with the endpoints resolving to its newest build |
602
+ | `GET` | `/api/categories` | Every category, with the endpoints resolving to its newest build |
603
603
  | `POST` | `/api/adopt`, `/api/adopt/candidates` | Take over what an engine or another node holds |
604
604
  | `POST` | `/api/sources/preview` | What a watched web location would take, without taking it |
605
605
  | `POST` | `/api/subscriptions/preview` | Whether a peer is reachable and what it offers |
@@ -156,7 +156,7 @@ the peer's to retract:
156
156
  Provenance is what makes this possible: an archive records which subscription
157
157
  sent it, and only that subscription can ever propose removing it.
158
158
 
159
- ## Sharing only what you tag
159
+ ## Sharing only what you categorise
160
160
 
161
161
  Category feeds let a *subscriber* narrow what it takes. They do not narrow what
162
162
  you publish: `/feed.xml` carries the whole catalogue, so a peer who could follow
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmtiles-swarm",
3
- "version": "0.4.5",
3
+ "version": "0.4.6",
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
@@ -843,7 +843,7 @@ export function createApp({
843
843
  // Tags, after the fact. They could only be set when an archive was added,
844
844
  // which is the wrong moment to have to know: a build becomes "weekly" when
845
845
  // there is a second one, and an archive is marked for sharing long after it
846
- // arrives. Accepts a whole list, or add/remove for one tag at a time.
846
+ // arrives. Accepts a whole list, or add/remove for one category at a time.
847
847
  app.patch(
848
848
  '/api/torrents/:infoHash/categories',
849
849
  route(async (req, res) => {
@@ -1426,7 +1426,7 @@ export function createApp({
1426
1426
  /**
1427
1427
  * Whether an archive may leave this node, for this caller.
1428
1428
  *
1429
- * Any tag matching is enough. A tag names one thing an archive is, not the
1429
+ * Any category matching is enough. A category names one thing an archive is, not the
1430
1430
  * whole of what it is, so a planet build tagged both "basemaps" and "weekly"
1431
1431
  * belongs in a basemaps feed whether or not weekly is also published.
1432
1432
  * @param {object} entry - Catalog entry.
package/src/catalog.js CHANGED
@@ -34,7 +34,7 @@ import path from 'node:path';
34
34
  * them. Accepts the older single `category` string so catalogues written before
35
35
  * tagging keep working — they are read as a list of one.
36
36
  * @param {object} source - Anything with `categories` and/or `category`.
37
- * @returns {string[]} - Sorted, de-duplicated, non-empty tags.
37
+ * @returns {string[]} - Sorted, de-duplicated, non-empty categories.
38
38
  */
39
39
  export function normalizeCategories(source) {
40
40
  const raw = [
@@ -117,7 +117,7 @@ export class Catalog {
117
117
  * @returns {CatalogEntry[]} - Matching entries.
118
118
  */
119
119
  byCategory(category) {
120
- // Any match, not all: a tag names one thing an archive is, and asking for
120
+ // Any match, not all: a category names one thing an archive is, and asking for
121
121
  // "terrain" should find everything tagged terrain whatever else it is.
122
122
  return this.list().filter((entry) =>
123
123
  normalizeCategories(entry).includes(category),
package/src/config.js CHANGED
@@ -643,7 +643,7 @@ const DEFAULTS = {
643
643
  * }
644
644
  *
645
645
  * Placeholders match a torrent client's, so an existing script keeps working:
646
- * %N name, %L first category, %G all tags, %F content path, %D save path,
646
+ * %N name, %L first category, %G all categories, %F content path, %D save path,
647
647
  * %Z size, %C file count, %I infohash.
648
648
  *
649
649
  * Command and arguments are separate rather than one string a shell pulls
package/src/hooks.js CHANGED
@@ -31,7 +31,7 @@ const TAIL_LINES = 20;
31
31
  * The set mirrors a torrent client's, so an existing script keeps working:
32
32
  *
33
33
  * %N name %I infohash %F content path
34
- * %L category %G tags %D save path
34
+ * %L category %G categories %D save path
35
35
  * %Z size %C file count
36
36
  *
37
37
  * @param {string} argument - An argument possibly containing placeholders.
@@ -346,7 +346,7 @@ export class SubscriptionManager {
346
346
  */
347
347
  async #add(item, subscription) {
348
348
  const options = {
349
- // A subscriber may file a peer's archives under its own tags; failing
349
+ // A subscriber may file a peer's archives under its own categories; failing
350
350
  // that, whatever the peer tagged them with comes across.
351
351
  categories:
352
352
  subscription.categories ??
@@ -357,7 +357,7 @@
357
357
  addressed by its infohash — which is what makes a tile immutable and
358
358
  cacheable for a year — so a style pointed at one is pinned to that
359
359
  build forever. These endpoints always resolve to the newest archive
360
- carrying the tag.
360
+ carrying the category.
361
361
  </div>
362
362
  <div id="category-list"></div>
363
363
  <div class="empty" id="categories-empty" hidden>
@@ -543,8 +543,8 @@
543
543
  <button type="button" id="add-category">Add</button>
544
544
  </div>
545
545
  <div class="sub">
546
- An archive can carry several. Feeds include anything tagged with
547
- the category asked for.
546
+ An archive can carry several. Feeds include every archive
547
+ carrying the category asked for.
548
548
  </div>
549
549
  </div>
550
550
 
@@ -2327,6 +2327,11 @@
2327
2327
  const { into, key, title, blurb, columns, rows, restart, preview, peerPreview, footnote } =
2328
2328
  spec;
2329
2329
  rowEditorColumns[key] = columns;
2330
+ // Kept so a save can put back what this editor never showed. An entry
2331
+ // holds more than there are columns for it — a watch folder's
2332
+ // pieceLength, a subscription's savePath — and a save that rebuilt each
2333
+ // record from the columns alone deleted every one of them.
2334
+ rowEditorRows[key] = rows;
2330
2335
  const panel = document.createElement('div');
2331
2336
  panel.className = 'panel';
2332
2337
  panel.style.marginBottom = '1rem';
@@ -2368,8 +2373,8 @@
2368
2373
  }
2369
2374
  </td>`;
2370
2375
 
2371
- const rowHtml = (row = {}) => `
2372
- <tr>
2376
+ const rowHtml = (row = {}, index) => `
2377
+ <tr${index === undefined ? '' : ` data-origin="${index}"`}>
2373
2378
  ${columns.map((column) => cell(column, row[column.field])).join('')}
2374
2379
  <td>
2375
2380
  ${preview ? '<button type="button" data-act="preview">Preview</button>' : ''}
@@ -2392,7 +2397,9 @@
2392
2397
  <th></th>
2393
2398
  </tr>
2394
2399
  </thead>
2395
- <tbody>${[...rows, {}].map((row) => rowHtml(row)).join('')}</tbody>
2400
+ <tbody>${[...rows, {}]
2401
+ .map((row, index) => rowHtml(row, index < rows.length ? index : undefined))
2402
+ .join('')}</tbody>
2396
2403
  </table>
2397
2404
  </div>
2398
2405
  <div style="margin-top:0.5rem">
@@ -2492,11 +2499,19 @@
2492
2499
  * @param {object[]} columns - Column definitions.
2493
2500
  * @returns {object} - The record.
2494
2501
  */
2495
- function readRow(row, columns) {
2496
- const record = {};
2502
+ function readRow(row, columns, original = {}) {
2503
+ // Starts from what was there. The editor shows the fields worth
2504
+ // offering, not every field an entry may hold, and rebuilding from the
2505
+ // columns alone silently dropped the rest.
2506
+ const record = { ...original };
2497
2507
  for (const column of columns) {
2498
2508
  const raw = row.querySelector(`[data-field="${column.field}"]`)?.value.trim() ?? '';
2499
- if (!raw) continue;
2509
+ // Emptied on purpose is still an instruction, so the field goes
2510
+ // rather than the original value surviving underneath it.
2511
+ if (!raw) {
2512
+ delete record[column.field];
2513
+ continue;
2514
+ }
2500
2515
  if (column.list) {
2501
2516
  record[column.field] = raw
2502
2517
  .split(',')
@@ -2527,8 +2542,19 @@
2527
2542
  for (const [key, columns] of Object.entries(specs)) {
2528
2543
  const panel = document.querySelector(`[data-row-editor="${key}"]`);
2529
2544
  if (!panel) continue;
2545
+ const originals = rowEditorRows[key] ?? [];
2530
2546
  const records = [...panel.querySelectorAll('tbody tr')]
2531
- .map((row) => readRow(row, columns))
2547
+ .map((row) => {
2548
+ // A row added in the console has no original; one rendered from
2549
+ // the configuration is matched back to it, so a removed row takes
2550
+ // its own hidden fields with it and the others keep theirs.
2551
+ const origin = row.dataset.origin;
2552
+ return readRow(
2553
+ row,
2554
+ columns,
2555
+ origin === undefined ? {} : (originals[Number(origin)] ?? {}),
2556
+ );
2557
+ })
2532
2558
  .filter((record) => record[columns[0].field]);
2533
2559
  updates[key] = records;
2534
2560
  }
@@ -3022,6 +3048,9 @@
3022
3048
  // ── Settings ──────────────────────────────────────────────────────────
3023
3049
  let restartKeys = new Set();
3024
3050
  let rowEditorColumns = {};
3051
+ // What each editor was rendered from, so a save can put back the fields
3052
+ // it never showed.
3053
+ let rowEditorRows = {};
3025
3054
 
3026
3055
  async function loadSettings() {
3027
3056
  const { config, restartRequired, configPath } = await api('/api/config');
@@ -3080,6 +3109,7 @@
3080
3109
  // are lists of small records, which is exactly what a torrent client
3081
3110
  // gives a grid for, and what a textarea full of braces is worst at.
3082
3111
  rowEditorColumns = {};
3112
+ rowEditorRows = {};
3083
3113
  await renderTokenEditor(body);
3084
3114
  renderHookEditor(body, config, restartKeys);
3085
3115
 
@@ -3352,7 +3382,12 @@
3352
3382
  placeholder: '1',
3353
3383
  number: true,
3354
3384
  },
3355
- { field: 'category', label: 'Tag as', placeholder: 'from-peer' },
3385
+ {
3386
+ field: 'categories',
3387
+ label: 'Categories',
3388
+ placeholder: 'from-peer, planet',
3389
+ list: true,
3390
+ },
3356
3391
  { field: 'filter', label: 'Name filter', placeholder: 'terrain' },
3357
3392
  { field: 'token', label: 'Token', placeholder: 'if issued one', secret: true },
3358
3393
  {