pmtiles-swarm 0.27.0 โ†’ 0.28.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,27 @@
7
7
  ### ๐Ÿž Bug fixes
8
8
  - _...Add new stuff here..._
9
9
 
10
+ ## 0.28.0
11
+ ### โœจ Features and improvements
12
+ - **The archive list can be searched and sorted, and says when each archive was added.** `Added` is
13
+ a column now โ€” `createdAt` has always been on every entry and returned by `/api/catalog`, it was
14
+ simply never shown โ€” with a date for anything older than today and a time for today, since the
15
+ question a list answers is which of these is recent rather than exactly when each arrived.
16
+
17
+ Beside it, the same filter and sort the public page has: by name, infohash or category, ordered
18
+ by newest added, oldest added, name or size โ€” and by download speed, upload speed or share
19
+ ratio, which are read off the live status the poll refreshes, so rows reorder themselves under
20
+ those every few seconds. That is what a torrent client does and what choosing "download speed"
21
+ asks for, and also why none of them is the default. Newest added is, because a list read
22
+ straight after adding something should have that thing at the top.
23
+
24
+ Both happen inside the render rather than where the data arrives. The list refreshes every three
25
+ seconds; filtering at the fetch would either clear what had been typed on each poll or refetch
26
+ the whole catalog on every keystroke. And the count beside the box says `4 of 37` while a filter
27
+ is narrowing things, because an empty table and a table filtered down to nothing look identical.
28
+
29
+ ### ๐Ÿž Bug fixes
30
+
10
31
  ## 0.27.0
11
32
  ### โœจ Features and improvements
12
33
  - **Bandwidth history per archive, kept across restarts.** The tile side of this question already
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmtiles-swarm",
3
- "version": "0.27.0",
3
+ "version": "0.28.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",
@@ -324,12 +324,36 @@
324
324
  <button type="button">Feed</button>
325
325
  </a>
326
326
  </div>
327
+ <div class="bar">
328
+ <input
329
+ id="archive-filter"
330
+ type="search"
331
+ placeholder="Filter by name, infohash or category"
332
+ autocomplete="off"
333
+ spellcheck="false"
334
+ style="flex:1;min-width:12rem"
335
+ />
336
+ <label>
337
+ Sort
338
+ <select id="archive-sort">
339
+ <option value="added">newest added</option>
340
+ <option value="oldest">oldest added</option>
341
+ <option value="name">name</option>
342
+ <option value="largest">largest</option>
343
+ <option value="down">download speed</option>
344
+ <option value="up">upload speed</option>
345
+ <option value="ratio">share ratio</option>
346
+ </select>
347
+ </label>
348
+ <span class="sub" id="archive-count"></span>
349
+ </div>
327
350
 
328
351
  <table>
329
352
  <thead>
330
353
  <tr>
331
354
  <th>Archive</th>
332
355
  <th>Size</th>
356
+ <th>Added</th>
333
357
  <th>Mode</th>
334
358
  <th>Origin</th>
335
359
  <th>Progress</th>
@@ -710,6 +734,56 @@
710
734
  const rate = (n) => (n > 0 ? `${bytes(n)}/s` : 'โ€”');
711
735
  const pct = (n) => `${Math.round((n ?? 0) * 100)}%`;
712
736
 
737
+ /**
738
+ * When an archive was added, as something short enough for a column.
739
+ *
740
+ * A date on its own for anything older than today, and a time for
741
+ * today: the question a list answers is "which of these is recent", and
742
+ * a full timestamp on every row is harder to scan than either.
743
+ * @param {string} value - ISO timestamp.
744
+ * @returns {string} - Markup for the cell.
745
+ */
746
+ const added = (value) => {
747
+ if (!value) return '<span class="sub">โ€”</span>';
748
+ const when = new Date(value);
749
+ if (Number.isNaN(when.getTime())) return '<span class="sub">โ€”</span>';
750
+ const today = new Date().toDateString() === when.toDateString();
751
+ return `<span title="${escapeHtml(when.toLocaleString())}">${
752
+ today
753
+ ? when.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
754
+ : when.toLocaleDateString()
755
+ }</span>`;
756
+ };
757
+
758
+ // The same three questions the public page asks, against an admin row.
759
+ // Newest first by default: a list read after adding something is a list
760
+ // where the thing just added should be at the top.
761
+ /**
762
+ * A live figure off an archive's status, or zero.
763
+ *
764
+ * An archive the engine has not reported on yet has no status at all,
765
+ * and comparing undefined gives NaN -- which makes a sort silently do
766
+ * nothing rather than fail, so it is worth the guard.
767
+ * @param {object} entry - Catalog entry with its status attached.
768
+ * @param {string} field - Which figure.
769
+ * @returns {number} - The value, or zero.
770
+ */
771
+ const speedOf = (entry, field) => Number(entry?.status?.[field] ?? 0) || 0;
772
+
773
+ const archiveSorters = {
774
+ added: (a, b) => new Date(b.createdAt ?? 0) - new Date(a.createdAt ?? 0),
775
+ oldest: (a, b) => new Date(a.createdAt ?? 0) - new Date(b.createdAt ?? 0),
776
+ name: (a, b) => String(a.name ?? '').localeCompare(String(b.name ?? '')),
777
+ largest: (a, b) => (b.size ?? 0) - (a.size ?? 0),
778
+ // Live values, read off the status the poll refreshes. Rows reorder
779
+ // themselves every few seconds under these, which is what a torrent
780
+ // client does and what somebody choosing "download speed" is asking
781
+ // for -- but it is also why they are not the default.
782
+ down: (a, b) => speedOf(b, 'downloadSpeed') - speedOf(a, 'downloadSpeed'),
783
+ up: (a, b) => speedOf(b, 'uploadSpeed') - speedOf(a, 'uploadSpeed'),
784
+ ratio: (a, b) => speedOf(b, 'ratio') - speedOf(a, 'ratio'),
785
+ };
786
+
713
787
  let toastTimer;
714
788
  const toast = (message) => {
715
789
  $('toast').textContent = message;
@@ -1004,9 +1078,34 @@
1004
1078
  function renderRows() {
1005
1079
  const rows = $('rows');
1006
1080
  rows.innerHTML = '';
1081
+
1082
+ // Filtered and sorted here rather than where the data arrives, so the
1083
+ // three-second poll keeps refreshing what is on screen without
1084
+ // resetting what was typed. `archives` stays the node's answer; this
1085
+ // is only the view of it.
1086
+ const needle = ($('archive-filter')?.value ?? '').trim().toLowerCase();
1087
+ const order = $('archive-sort')?.value ?? 'added';
1088
+ const shown = archives
1089
+ .filter(
1090
+ (entry) =>
1091
+ needle === '' ||
1092
+ String(entry.name ?? '').toLowerCase().includes(needle) ||
1093
+ String(entry.infoHash ?? '').toLowerCase().includes(needle) ||
1094
+ (entry.categories ?? []).some((tag) =>
1095
+ String(tag).toLowerCase().includes(needle),
1096
+ ),
1097
+ )
1098
+ .sort(archiveSorters[order] ?? archiveSorters.added);
1099
+
1007
1100
  $('empty').hidden = archives.length > 0;
1101
+ // Says when a filter is hiding things, because an empty table and a
1102
+ // table filtered down to nothing look identical otherwise.
1103
+ $('archive-count').textContent =
1104
+ needle === ''
1105
+ ? ''
1106
+ : `${shown.length} of ${archives.length}`;
1008
1107
 
1009
- for (const entry of archives) {
1108
+ for (const entry of shown) {
1010
1109
  const s = entry.status ?? {};
1011
1110
  const progress = s.progress ?? 0;
1012
1111
  const mode = entry.mode ?? 'mirror';
@@ -1024,6 +1123,7 @@
1024
1123
  ? `<div class="sub">${escapeHtml(entry.kind)} ยท not servable</div>`
1025
1124
  : ''
1026
1125
  }</td>
1126
+ <td>${added(entry.createdAt)}</td>
1027
1127
  <td><span class="pill ${mode}">${mode}</span></td>
1028
1128
  <td>${originCell(entry)}</td>
1029
1129
  <td>
@@ -4204,6 +4304,8 @@
4204
4304
  $('traffic-live').onchange = trafficPoll;
4205
4305
  trafficPoll();
4206
4306
 
4307
+ $('archive-filter').addEventListener('input', renderRows);
4308
+ $('archive-sort').addEventListener('change', renderRows);
4207
4309
  $('traffic-window').onchange = () => loadSwarmTraffic();
4208
4310
  $('traffic-reset').onclick = async () => {
4209
4311
  // Deliberately separate from reading: a page polling the endpoint must