signalk-charts-provider-simple 2.0.2 → 2.0.3

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/README.md CHANGED
@@ -107,9 +107,9 @@ The interface provides four tabs:
107
107
  - **Node.js >= 22.5** — uses the built-in `node:sqlite` module, no native compilation needed
108
108
  - **Not supported on Cerbo GX** — Venus OS ships Node.js 20, which lacks the `node:sqlite` module. Use v1.6.x if you need Cerbo support.
109
109
 
110
- ### Required for chart conversion: `signalk-container` plugin
110
+ ### Recommended for chart conversion: `signalk-container` plugin
111
111
 
112
- To convert S-57 ENC, BSB raster, Pilot Charts, or basemaps, this plugin delegates all container work to the [`signalk-container`](https://github.com/dirkwa/signalk-container) plugin. The Signal K App Store handles installing it for you when you install Charts Provider Simple — the plugin's detail page shows it as a required plugin and offers a one-click bulk install.
112
+ Displaying charts (serving tiles from `.mbtiles`) needs nothing extra. The [`signalk-container`](https://github.com/dirkwa/signalk-container) plugin is only needed to **convert** charts (S-57 ENC, BSB raster, Pilot Charts, or basemaps), where this plugin delegates all container work to it. The Signal K App Store lists it as a recommended plugin on the Charts Provider Simple detail page and offers a one-click bulk install; without it the plugin still loads and serves charts — only the Convert tab is disabled.
113
113
 
114
114
  `signalk-container` itself needs a Docker- or Podman-compatible runtime on the host. Both engines work the same way; pick whichever is easier:
115
115
 
@@ -133,7 +133,7 @@ The plugin uses one combined image that `signalk-container` pulls automatically
133
133
  - **Signal K in Docker / Podman with a named volume** — the helper container mounts the same named volume. (1.x couldn't do this; named-volume deployments were unable to convert.)
134
134
  - **Signal K in Docker / Podman with bind-mounted data** — the helper container gets the resolved host path, even when the in-container path doesn't match the host path. (1.x relied on host/container paths matching exactly; mismatches silently failed.)
135
135
 
136
- **Signal K running in Docker?** See [docs/running-in-docker.md](docs/running-in-docker.md) for the docker-compose snippet.
136
+ **Signal K running in Docker?** Runtime and socket setup is handled by `signalk-container` — see [docs/running-in-docker.md](docs/running-in-docker.md) for how conversion is delegated and where to configure the host.
137
137
 
138
138
  Conversion concurrency is configurable — see the [CPU budget](#cpu-budget-for-chart-conversion) section. MBTiles charts (display only, no conversion) work without any container runtime, and without `signalk-container`.
139
139
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "signalk-charts-provider-simple",
3
- "version": "2.0.2",
3
+ "version": "2.0.3",
4
4
  "description": "Simple Signal K chart provider for local MBTiles with web and download management",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -382,6 +382,10 @@ select.catalog-folder-select:hover {
382
382
  appearance: auto;
383
383
  }
384
384
 
385
+ option.catalog-folder-option-new {
386
+ color: var(--md-sys-color-primary);
387
+ }
388
+
385
389
  /* Source attribution note */
386
390
  .catalog-source-note {
387
391
  padding: var(--md-sys-spacing-3) var(--md-sys-spacing-4);
@@ -364,7 +364,7 @@ function renderCatalogCard(catalog) {
364
364
  </div>
365
365
  </div>
366
366
  <div class="catalog-card-body" id="catalog-body-${catalogEscapeId(catalog.file)}">
367
- ${isExpanded && catalogChartData[catalog.file] ? renderChartList(catalog.file) : ''}
367
+ ${isExpanded && catalogChartData[catalog.file] ? renderChartList(catalog.file, catalog.label) : ''}
368
368
  </div>
369
369
  </div>
370
370
  `;
@@ -408,11 +408,12 @@ async function toggleCatalog(catalogFile) {
408
408
  // Re-render to show charts
409
409
  renderCatalogList();
410
410
  }
411
- function renderChartList(catalogFile) {
411
+ function renderChartList(catalogFile, catalogLabel) {
412
412
  const data = catalogChartData[catalogFile];
413
413
  if (!data?.charts || data.charts.length === 0) {
414
414
  return `<div class="catalog-empty">No charts in this catalog.</div>`;
415
415
  }
416
+ const defaultFolder = catalogLabelToFolder(catalogLabel);
416
417
  return data.charts
417
418
  .map((chart) => {
418
419
  const cls = chart.urlClassification ?? {
@@ -495,7 +496,7 @@ function renderChartList(catalogFile) {
495
496
  ${podmanHint}
496
497
  ${zoomHtml}
497
498
  <select class="catalog-folder-select" id="catalog-folder-${catalogEscapeId(chart.number)}">
498
- ${catalogFolders.map((f) => `<option value="${catalogEscapeAttr(f)}">${catalogEscapeHtml(f)}</option>`).join('')}
499
+ ${buildFolderOptions(defaultFolder)}
499
500
  </select>
500
501
  <button class="btn-catalog-download" ${btnDisabled}
501
502
  data-catalog-download="${catalogEscapeAttr(chart.number)}"
@@ -926,6 +927,40 @@ function categoryLabel(category) {
926
927
  };
927
928
  return labels[category] ?? category;
928
929
  }
930
+ /**
931
+ * Derive a filesystem-safe folder name from a catalog label.
932
+ * Strips path separators and illegal characters, collapses whitespace, removes
933
+ * leading dots/spaces, and rejects a result of '.' or '..' so a label can't
934
+ * resolve to a traversal or a hidden/relative folder. Falls back to '/' when
935
+ * nothing usable remains.
936
+ */
937
+ function catalogLabelToFolder(label) {
938
+ if (!label) {
939
+ return '/';
940
+ }
941
+ const safe = label
942
+ .replace(/[/\\:*?"<>|]/g, '')
943
+ .replace(/\s+/g, ' ')
944
+ .trim()
945
+ .replace(/^[.\s]+/, '')
946
+ .trim();
947
+ return safe && safe !== '.' && safe !== '..' ? safe : '/';
948
+ }
949
+ function folderDisplayName(folder) {
950
+ return folder === '/' ? '/' : `/${folder}`;
951
+ }
952
+ function buildFolderOptions(defaultFolder) {
953
+ const isNew = defaultFolder !== '/' && !catalogFolders.includes(defaultFolder);
954
+ const options = [];
955
+ if (isNew) {
956
+ options.push(`<option value="${catalogEscapeAttr(defaultFolder)}" selected class="catalog-folder-option-new">${catalogEscapeHtml(folderDisplayName(defaultFolder))} (new)</option>`);
957
+ }
958
+ for (const folder of catalogFolders) {
959
+ const selected = !isNew && folder === defaultFolder ? ' selected' : '';
960
+ options.push(`<option value="${catalogEscapeAttr(folder)}"${selected}>${catalogEscapeHtml(folderDisplayName(folder))}</option>`);
961
+ }
962
+ return options.join('');
963
+ }
929
964
  function catalogEscapeHtml(str) {
930
965
  if (!str) {
931
966
  return '';
@@ -481,7 +481,7 @@ function renderCatalogCard(catalog: CatalogRegistryEntry): string {
481
481
  </div>
482
482
  </div>
483
483
  <div class="catalog-card-body" id="catalog-body-${catalogEscapeId(catalog.file)}">
484
- ${isExpanded && catalogChartData[catalog.file] ? renderChartList(catalog.file) : ''}
484
+ ${isExpanded && catalogChartData[catalog.file] ? renderChartList(catalog.file, catalog.label) : ''}
485
485
  </div>
486
486
  </div>
487
487
  `;
@@ -533,11 +533,12 @@ async function toggleCatalog(catalogFile: string): Promise<void> {
533
533
  renderCatalogList();
534
534
  }
535
535
 
536
- function renderChartList(catalogFile: string): string {
536
+ function renderChartList(catalogFile: string, catalogLabel: string): string {
537
537
  const data = catalogChartData[catalogFile];
538
538
  if (!data?.charts || data.charts.length === 0) {
539
539
  return `<div class="catalog-empty">No charts in this catalog.</div>`;
540
540
  }
541
+ const defaultFolder = catalogLabelToFolder(catalogLabel);
541
542
 
542
543
  return data.charts
543
544
  .map((chart) => {
@@ -625,7 +626,7 @@ function renderChartList(catalogFile: string): string {
625
626
  ${podmanHint}
626
627
  ${zoomHtml}
627
628
  <select class="catalog-folder-select" id="catalog-folder-${catalogEscapeId(chart.number)}">
628
- ${catalogFolders.map((f) => `<option value="${catalogEscapeAttr(f)}">${catalogEscapeHtml(f)}</option>`).join('')}
629
+ ${buildFolderOptions(defaultFolder)}
629
630
  </select>
630
631
  <button class="btn-catalog-download" ${btnDisabled}
631
632
  data-catalog-download="${catalogEscapeAttr(chart.number)}"
@@ -1092,6 +1093,50 @@ function categoryLabel(category: CatalogCategory | string): string {
1092
1093
  return labels[category] ?? category;
1093
1094
  }
1094
1095
 
1096
+ /**
1097
+ * Derive a filesystem-safe folder name from a catalog label.
1098
+ * Strips path separators and illegal characters, collapses whitespace, removes
1099
+ * leading dots/spaces, and rejects a result of '.' or '..' so a label can't
1100
+ * resolve to a traversal or a hidden/relative folder. Falls back to '/' when
1101
+ * nothing usable remains.
1102
+ */
1103
+ function catalogLabelToFolder(label: string | undefined | null): string {
1104
+ if (!label) {
1105
+ return '/';
1106
+ }
1107
+ const safe = label
1108
+ .replace(/[/\\:*?"<>|]/g, '')
1109
+ .replace(/\s+/g, ' ')
1110
+ .trim()
1111
+ .replace(/^[.\s]+/, '')
1112
+ .trim();
1113
+ return safe && safe !== '.' && safe !== '..' ? safe : '/';
1114
+ }
1115
+
1116
+ function folderDisplayName(folder: string): string {
1117
+ return folder === '/' ? '/' : `/${folder}`;
1118
+ }
1119
+
1120
+ function buildFolderOptions(defaultFolder: string): string {
1121
+ const isNew = defaultFolder !== '/' && !catalogFolders.includes(defaultFolder);
1122
+ const options: string[] = [];
1123
+
1124
+ if (isNew) {
1125
+ options.push(
1126
+ `<option value="${catalogEscapeAttr(defaultFolder)}" selected class="catalog-folder-option-new">${catalogEscapeHtml(folderDisplayName(defaultFolder))} (new)</option>`
1127
+ );
1128
+ }
1129
+
1130
+ for (const folder of catalogFolders) {
1131
+ const selected = !isNew && folder === defaultFolder ? ' selected' : '';
1132
+ options.push(
1133
+ `<option value="${catalogEscapeAttr(folder)}"${selected}>${catalogEscapeHtml(folderDisplayName(folder))}</option>`
1134
+ );
1135
+ }
1136
+
1137
+ return options.join('');
1138
+ }
1139
+
1095
1140
  function catalogEscapeHtml(str: string | undefined | null): string {
1096
1141
  if (!str) {
1097
1142
  return '';
@@ -36,8 +36,11 @@ async function initConvertTab() {
36
36
  ${!convertPodmanAvailable
37
37
  ? `
38
38
  <div class="catalog-podman-warning">
39
- <strong>Container runtime not reachable.</strong>
40
- Chart conversion needs a Docker- or Podman-compatible socket.
39
+ <strong>Chart conversion unavailable.</strong>
40
+ Converting charts needs the
41
+ <a href="https://github.com/dirkwa/signalk-container" target="_blank" rel="noopener">signalk-container</a>
42
+ plugin (install it from the App Store) plus a Docker- or Podman-compatible
43
+ runtime on the host. Chart display works without it.
41
44
  <a href="https://github.com/dirkwa/signalk-charts-provider-simple/blob/main/docs/running-in-docker.md" target="_blank" rel="noopener">See setup notes</a>.
42
45
  </div>
43
46
  `
@@ -143,7 +146,7 @@ function setupDropZone(zoneId, inputId) {
143
146
  }
144
147
  zone.onclick = () => {
145
148
  if (!convertPodmanAvailable) {
146
- alert('Chart conversion needs a Docker- or Podman-compatible socket. See the warning above.');
149
+ alert('Chart conversion needs the signalk-container plugin and a Docker- or Podman-compatible runtime. See the warning above.');
147
150
  return;
148
151
  }
149
152
  input.click();
@@ -159,7 +162,7 @@ function setupDropZone(zoneId, inputId) {
159
162
  e.preventDefault();
160
163
  zone.classList.remove('dragover');
161
164
  if (!convertPodmanAvailable) {
162
- alert('Chart conversion needs a Docker- or Podman-compatible socket. See the warning above.');
165
+ alert('Chart conversion needs the signalk-container plugin and a Docker- or Podman-compatible runtime. See the warning above.');
163
166
  return;
164
167
  }
165
168
  const dt = e.dataTransfer;
@@ -61,8 +61,11 @@ async function initConvertTab(): Promise<void> {
61
61
  !convertPodmanAvailable
62
62
  ? `
63
63
  <div class="catalog-podman-warning">
64
- <strong>Container runtime not reachable.</strong>
65
- Chart conversion needs a Docker- or Podman-compatible socket.
64
+ <strong>Chart conversion unavailable.</strong>
65
+ Converting charts needs the
66
+ <a href="https://github.com/dirkwa/signalk-container" target="_blank" rel="noopener">signalk-container</a>
67
+ plugin (install it from the App Store) plus a Docker- or Podman-compatible
68
+ runtime on the host. Chart display works without it.
66
69
  <a href="https://github.com/dirkwa/signalk-charts-provider-simple/blob/main/docs/running-in-docker.md" target="_blank" rel="noopener">See setup notes</a>.
67
70
  </div>
68
71
  `
@@ -178,7 +181,7 @@ function setupDropZone(zoneId: string, inputId: string): void {
178
181
  zone.onclick = () => {
179
182
  if (!convertPodmanAvailable) {
180
183
  alert(
181
- 'Chart conversion needs a Docker- or Podman-compatible socket. See the warning above.'
184
+ 'Chart conversion needs the signalk-container plugin and a Docker- or Podman-compatible runtime. See the warning above.'
182
185
  );
183
186
  return;
184
187
  }
@@ -199,7 +202,7 @@ function setupDropZone(zoneId: string, inputId: string): void {
199
202
  zone.classList.remove('dragover');
200
203
  if (!convertPodmanAvailable) {
201
204
  alert(
202
- 'Chart conversion needs a Docker- or Podman-compatible socket. See the warning above.'
205
+ 'Chart conversion needs the signalk-container plugin and a Docker- or Podman-compatible runtime. See the warning above.'
203
206
  );
204
207
  return;
205
208
  }