signalk-charts-provider-simple 2.1.0 → 2.2.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.
Files changed (38) hide show
  1. package/README.md +2 -2
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +71 -35
  4. package/dist/index.js.map +1 -1
  5. package/dist/types.d.ts +16 -3
  6. package/dist/types.d.ts.map +1 -1
  7. package/dist/utils/catalog-manager.d.ts +15 -1
  8. package/dist/utils/catalog-manager.d.ts.map +1 -1
  9. package/dist/utils/catalog-manager.js +193 -4
  10. package/dist/utils/catalog-manager.js.map +1 -1
  11. package/dist/utils/catalog-schemas.d.ts +6 -4
  12. package/dist/utils/catalog-schemas.d.ts.map +1 -1
  13. package/dist/utils/catalog-schemas.js +10 -3
  14. package/dist/utils/catalog-schemas.js.map +1 -1
  15. package/dist/utils/container-fs.d.ts +17 -0
  16. package/dist/utils/container-fs.d.ts.map +1 -0
  17. package/dist/utils/container-fs.js +40 -0
  18. package/dist/utils/container-fs.js.map +1 -0
  19. package/dist/utils/container-images.d.ts +1 -1
  20. package/dist/utils/container-images.js +1 -1
  21. package/dist/utils/download-manager.d.ts +5 -0
  22. package/dist/utils/download-manager.d.ts.map +1 -1
  23. package/dist/utils/download-manager.js +97 -13
  24. package/dist/utils/download-manager.js.map +1 -1
  25. package/dist/utils/quarantine.d.ts.map +1 -1
  26. package/dist/utils/quarantine.js +2 -0
  27. package/dist/utils/quarantine.js.map +1 -1
  28. package/dist/utils/rnc-converter.d.ts.map +1 -1
  29. package/dist/utils/rnc-converter.js +2 -2
  30. package/dist/utils/rnc-converter.js.map +1 -1
  31. package/dist/utils/s57-converter.d.ts +1 -1
  32. package/dist/utils/s57-converter.d.ts.map +1 -1
  33. package/dist/utils/s57-converter.js +13 -6
  34. package/dist/utils/s57-converter.js.map +1 -1
  35. package/package.json +1 -1
  36. package/public/css/chart-catalog.css +171 -1
  37. package/public/js/chart-catalog.js +526 -18
  38. package/public/js/chart-catalog.ts +591 -18
package/README.md CHANGED
@@ -125,7 +125,7 @@ systemctl --user enable --now podman.socket
125
125
 
126
126
  The plugin uses one combined image that `signalk-container` pulls automatically on first conversion:
127
127
 
128
- - `ghcr.io/dirkwa/signalk-charts-provider-simple/charts-toolbox:1.0.0` — GDAL + tippecanoe + tile-join + helpers in a single image (multi-arch: amd64 + arm64). The plugin pins to a specific `:VERSION` tag rather than `:latest` so a published image tag is permanent for any host that pulled it; bumping the toolbox image is a co-ordinated edit of `docker/charts-toolbox/VERSION` and the matching constant in `src/utils/container-images.ts`.
128
+ - `ghcr.io/dirkwa/signalk-charts-provider-simple/charts-toolbox:1.1.0` — GDAL + tippecanoe + tile-join + helpers in a single image (multi-arch: amd64 + arm64). The plugin pins to a specific `:VERSION` tag rather than `:latest` so a published image tag is permanent for any host that pulled it; bumping the toolbox image is a co-ordinated edit of `docker/charts-toolbox/VERSION` and the matching constant in `src/utils/container-images.ts`.
129
129
 
130
130
  **Why `signalk-container`?** It transparently handles the three deployment topologies that 1.x got wrong:
131
131
 
@@ -144,7 +144,7 @@ The same `charts-toolbox` image the plugin uses internally is also available for
144
144
  Pull it directly from GHCR:
145
145
 
146
146
  ```bash
147
- docker pull ghcr.io/dirkwa/signalk-charts-provider-simple/charts-toolbox:1.0.0
147
+ docker pull ghcr.io/dirkwa/signalk-charts-provider-simple/charts-toolbox:1.1.0
148
148
  ```
149
149
 
150
150
  See [docs/charts-toolbox-image.md](docs/charts-toolbox-image.md) for a quickstart, the `--user` / `--userns=keep-id` flag forms per runtime, and reproducibility notes.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAQ,MAAM,qBAAqB,CAAC;AAiExD,OAAO,KAAK,EACV,iBAAiB,EAQlB,MAAM,YAAY,CAAC;AAcpB,QAAA,MAAM,iBAAiB,GAAI,KAAK,iBAAiB,KAAG,MAwyFnD,CAAC;AAiHF,eAAe,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAQ,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAuExD,OAAO,KAAK,EAGV,iBAAiB,EAMlB,MAAM,YAAY,CAAC;AAepB,QAAA,MAAM,iBAAiB,GAAI,KAAK,iBAAiB,KAAG,MA+0FnD,CAAC;AAiHF,eAAe,iBAAiB,CAAC"}
package/dist/index.js CHANGED
@@ -1,29 +1,28 @@
1
- import path from 'path';
1
+ import { SKVersion } from '@signalk/server-api';
2
+ import { Type } from '@sinclair/typebox';
3
+ import Busboy from 'busboy';
2
4
  import fs from 'fs';
3
5
  import https from 'https';
4
- import { SKVersion } from '@signalk/server-api';
6
+ import { DatabaseSync } from 'node:sqlite';
7
+ import path from 'path';
5
8
  import { findCharts, findRepairableCharts } from './charts-loader.js';
6
- import { scanChartsRecursively, scanAllFolders } from './utils/file-scanner.js';
9
+ import { checkForUpdates, classifyUrl, fetchCatalog, fetchCatalogRegistry, getCachedCatalog, getCatalogRegistry, getCatalogsWithInstalledCharts, getConvertingCharts, getConvertingCount, getInstalledCatalogCharts, getRegistryStatus, initCatalogManager, pruneStaleInstalls, removeInstall, removeInstallByFilename, renameInstallFilename, rollbackInstall, setConvertingState, setInstallFilename, trackInstall } from './utils/catalog-manager.js';
10
+ import { cleanCatalogTitle } from './utils/catalog-title.js';
7
11
  import { initChartState, isChartEnabled, setChartEnabled } from './utils/chart-state.js';
8
- import { downloadManager } from './utils/download-manager.js';
9
- import { initCatalogManager, getCatalogRegistry, fetchCatalog, getCachedCatalog, classifyUrl, trackInstall, setInstallFilename, renameInstallFilename, removeInstall, removeInstallByFilename, getInstalledCatalogCharts, pruneStaleInstalls, setConvertingState, getConvertingCharts, getConvertingCount, checkForUpdates, getCatalogsWithInstalledCharts } from './utils/catalog-manager.js';
10
- import { initS57Converter, processS57Zip, getAllConversionProgress as getAllS57Progress, getConversionProgress as getS57Progress, setConversionFailed as setS57Failed } from './utils/s57-converter.js';
11
- import { getContainerManager, waitForContainerManager } from './utils/container-manager.js';
12
+ import { getCpuBudget, setCpuBudget } from './utils/concurrency.js';
12
13
  import { PLUGIN_OWNER_ID } from './utils/container-jobs.js';
13
- import { cleanupQuarantineDir, makeQuarantineDir, promoteQuarantine, sweepStaleQuarantineDirs } from './utils/quarantine.js';
14
- import { cleanCatalogTitle } from './utils/catalog-title.js';
15
- import { setMbtilesDisplayName, repairMbtilesMetadata } from './utils/mbtiles-metadata.js';
14
+ import { getContainerManager, waitForContainerManager } from './utils/container-manager.js';
15
+ import { downloadManager } from './utils/download-manager.js';
16
+ import { scanAllFolders, scanChartsRecursively } from './utils/file-scanner.js';
17
+ import { repairMbtilesMetadata, setMbtilesDisplayName } from './utils/mbtiles-metadata.js';
16
18
  import { open as openMbtilesReader } from './utils/mbtiles-reader.js';
17
- import { initRncConverter, processRncZip, processPilotTar, getAllConversionProgress as getAllRncProgress, getConversionProgress as getRncProgress, setConversionFailed as setRncFailed } from './utils/rnc-converter.js';
18
- import { processGshhg, processShpBasemap } from './utils/s57-converter.js';
19
- import { getCpuBudget, setCpuBudget } from './utils/concurrency.js';
20
19
  import { writeChartPathMarker } from './utils/path-marker.js';
20
+ import { arePairWithinBase, isWithinBase, validateChartName } from './utils/path-safety.js';
21
21
  import { parsePluginConfig } from './utils/plugin-config-schema.js';
22
- import { Type } from '@sinclair/typebox';
22
+ import { cleanupQuarantineDir, makeQuarantineDir, promoteQuarantine, sweepStaleQuarantineDirs } from './utils/quarantine.js';
23
23
  import { parseBody, parseShape } from './utils/rest-validation.js';
24
- import { isWithinBase, arePairWithinBase, validateChartName } from './utils/path-safety.js';
25
- import Busboy from 'busboy';
26
- import { DatabaseSync } from 'node:sqlite';
24
+ import { getAllConversionProgress as getAllRncProgress, getConversionProgress as getRncProgress, initRncConverter, processPilotTar, processRncZip, setConversionFailed as setRncFailed } from './utils/rnc-converter.js';
25
+ import { getAllConversionProgress as getAllS57Progress, getConversionProgress as getS57Progress, initS57Converter, processGshhg, processS57Zip, processShpBasemap, setConversionFailed as setS57Failed } from './utils/s57-converter.js';
27
26
  // JSON import with type attribute (NodeNext ESM). package.json's `version`
28
27
  // is what the marker file records so users can confirm the running build.
29
28
  import packageJson from '../package.json' with { type: 'json' };
@@ -320,7 +319,14 @@ const pluginConstructor = (app) => {
320
319
  `rolling back chart ${chartNumber ?? '<unknown>'}`);
321
320
  if (chartNumber) {
322
321
  setConvertingState(chartNumber, false);
323
- removeInstall(chartNumber);
322
+ // A restart/crash can land here mid-update. rollbackInstall
323
+ // restores the prior on-disk version (UPDATE) or drops the
324
+ // pending record (FRESH), reading the snapshot trackInstall
325
+ // persisted in the record — so a still-pending update keeps
326
+ // surfacing in checkForUpdates(). For a committed record (no
327
+ // snapshot marker) it's a no-op, so a spurious reap of an
328
+ // already-installed chart doesn't delete it.
329
+ rollbackInstall(chartNumber);
324
330
  }
325
331
  }
326
332
  if (cleanup.reaped.length > 0) {
@@ -1467,13 +1473,43 @@ const pluginConstructor = (app) => {
1467
1473
  const registry = getCatalogRegistry();
1468
1474
  const installed = getInstalledCatalogCharts();
1469
1475
  const convertingCharts = getConvertingCharts();
1470
- res.json({ registry, installed, converting: convertingCharts });
1476
+ res.json({
1477
+ registry,
1478
+ installed,
1479
+ converting: convertingCharts,
1480
+ registryStatus: getRegistryStatus()
1481
+ });
1471
1482
  }
1472
1483
  catch (error) {
1473
1484
  console.error('Error fetching catalog registry:', error);
1474
1485
  res.status(500).json({ error: 'Failed to fetch catalog registry' });
1475
1486
  }
1476
1487
  });
1488
+ // Force a re-fetch of the catalog index from GitHub (the Refresh button).
1489
+ // Awaits the fetch so the response reflects the just-attempted result, but
1490
+ // swallows the rejection — registryStatus carries the reason (rate-limited
1491
+ // / offline / error) so the UI can message accurately.
1492
+ router.post('/catalog-registry/refresh', (_req, res) => {
1493
+ void (async () => {
1494
+ try {
1495
+ await fetchCatalogRegistry().catch((err) => {
1496
+ app.debug(`Catalog registry refresh failed: ${err instanceof Error ? err.message : String(err)}`);
1497
+ });
1498
+ res.json({
1499
+ registry: getCatalogRegistry(),
1500
+ installed: getInstalledCatalogCharts(),
1501
+ converting: getConvertingCharts(),
1502
+ registryStatus: getRegistryStatus()
1503
+ });
1504
+ }
1505
+ catch (error) {
1506
+ // The refresh itself is awaited+swallowed above; this only fires if
1507
+ // building the response throws.
1508
+ console.error('Error building catalog refresh response:', error);
1509
+ res.status(500).json({ error: 'Failed to build catalog refresh response' });
1510
+ }
1511
+ })();
1512
+ });
1477
1513
  router.get('/catalog-s57-status', (_req, res) => {
1478
1514
  // Runtime info is owned by signalk-container in 2.0+; we report
1479
1515
  // whatever its manager exposes. Available = manager present AND
@@ -1791,14 +1827,14 @@ const pluginConstructor = (app) => {
1791
1827
  void (async () => {
1792
1828
  try {
1793
1829
  if (!job.extractedFiles || job.extractedFiles.length === 0) {
1794
- removeInstall(chartNumber);
1830
+ rollbackInstall(chartNumber);
1795
1831
  app.debug(`Removed catalog tracking for ${chartNumber}: no .mbtiles extracted`);
1796
1832
  return;
1797
1833
  }
1798
1834
  if (job.status !== 'completed') {
1799
1835
  // Failed downloads leak nothing into the live target;
1800
1836
  // the quarantine wipe in `finally` handles cleanup.
1801
- removeInstall(chartNumber);
1837
+ rollbackInstall(chartNumber);
1802
1838
  return;
1803
1839
  }
1804
1840
  try {
@@ -1806,7 +1842,7 @@ const pluginConstructor = (app) => {
1806
1842
  }
1807
1843
  catch (promoteErr) {
1808
1844
  app.error(`Promotion failed for ${chartNumber}: ${promoteErr instanceof Error ? promoteErr.message : String(promoteErr)}`);
1809
- removeInstall(chartNumber);
1845
+ rollbackInstall(chartNumber);
1810
1846
  return;
1811
1847
  }
1812
1848
  // The global `job-completed` handler enables charts and
@@ -1890,7 +1926,7 @@ const pluginConstructor = (app) => {
1890
1926
  const zipPath = zipFileName ? path.join(tmpDownloadDir, zipFileName) : null;
1891
1927
  if (!zipPath || !fs.existsSync(zipPath)) {
1892
1928
  app.debug(`S-57: no ZIP file found after download for ${chartNumber}`);
1893
- removeInstall(chartNumber);
1929
+ rollbackInstall(chartNumber);
1894
1930
  cleanupDir(tmpDownloadDir);
1895
1931
  return;
1896
1932
  }
@@ -1937,7 +1973,7 @@ const pluginConstructor = (app) => {
1937
1973
  }
1938
1974
  catch (convError) {
1939
1975
  app.error(`S-57 conversion failed for ${chartNumber}: ${convError instanceof Error ? convError.message : String(convError)}`);
1940
- removeInstall(chartNumber);
1976
+ rollbackInstall(chartNumber);
1941
1977
  setConvertingState(chartNumber, false);
1942
1978
  if (quarantineDir !== null) {
1943
1979
  cleanupQuarantineDir(quarantineDir);
@@ -1951,7 +1987,7 @@ const pluginConstructor = (app) => {
1951
1987
  }
1952
1988
  downloadManager.removeListener('job-completed', s57Listener);
1953
1989
  downloadManager.removeListener('job-failed', s57FailListener);
1954
- removeInstall(chartNumber);
1990
+ rollbackInstall(chartNumber);
1955
1991
  setConvertingState(chartNumber, false);
1956
1992
  setS57Failed(chartNumber, job.error ?? 'Download failed');
1957
1993
  cleanupDir(tmpDownloadDir);
@@ -1978,7 +2014,7 @@ const pluginConstructor = (app) => {
1978
2014
  const zipPath = zipFileName ? path.join(tmpDownloadDir, zipFileName) : null;
1979
2015
  if (!zipPath || !fs.existsSync(zipPath)) {
1980
2016
  app.debug(`RNC: no file found after download for ${chartNumber}`);
1981
- removeInstall(chartNumber);
2017
+ rollbackInstall(chartNumber);
1982
2018
  cleanupDir(tmpDownloadDir);
1983
2019
  return;
1984
2020
  }
@@ -2020,7 +2056,7 @@ const pluginConstructor = (app) => {
2020
2056
  }
2021
2057
  catch (convError) {
2022
2058
  app.error(`RNC conversion failed for ${chartNumber}: ${convError instanceof Error ? convError.message : String(convError)}`);
2023
- removeInstall(chartNumber);
2059
+ rollbackInstall(chartNumber);
2024
2060
  setConvertingState(chartNumber, false);
2025
2061
  if (quarantineDir !== null) {
2026
2062
  cleanupQuarantineDir(quarantineDir);
@@ -2034,7 +2070,7 @@ const pluginConstructor = (app) => {
2034
2070
  }
2035
2071
  downloadManager.removeListener('job-completed', rncListener);
2036
2072
  downloadManager.removeListener('job-failed', rncFailListener);
2037
- removeInstall(chartNumber);
2073
+ rollbackInstall(chartNumber);
2038
2074
  setConvertingState(chartNumber, false);
2039
2075
  setRncFailed(chartNumber, job.error ?? 'Download failed');
2040
2076
  cleanupDir(tmpDownloadDir);
@@ -2060,7 +2096,7 @@ const pluginConstructor = (app) => {
2060
2096
  const dlFileName = job.extractedFiles?.[0] ?? job.targetFiles?.[0];
2061
2097
  const dlPath = dlFileName ? path.join(tmpDownloadDir, dlFileName) : null;
2062
2098
  if (!dlPath || !fs.existsSync(dlPath)) {
2063
- removeInstall(chartNumber);
2099
+ rollbackInstall(chartNumber);
2064
2100
  cleanupDir(tmpDownloadDir);
2065
2101
  return;
2066
2102
  }
@@ -2091,7 +2127,7 @@ const pluginConstructor = (app) => {
2091
2127
  }
2092
2128
  catch (convError) {
2093
2129
  app.error(`Pilot conversion failed: ${convError instanceof Error ? convError.message : String(convError)}`);
2094
- removeInstall(chartNumber);
2130
+ rollbackInstall(chartNumber);
2095
2131
  setConvertingState(chartNumber, false);
2096
2132
  if (quarantineDir !== null) {
2097
2133
  cleanupQuarantineDir(quarantineDir);
@@ -2105,7 +2141,7 @@ const pluginConstructor = (app) => {
2105
2141
  }
2106
2142
  downloadManager.removeListener('job-completed', pilotListener);
2107
2143
  downloadManager.removeListener('job-failed', pilotFailListener);
2108
- removeInstall(chartNumber);
2144
+ rollbackInstall(chartNumber);
2109
2145
  setConvertingState(chartNumber, false);
2110
2146
  setRncFailed(chartNumber, job.error ?? 'Download failed');
2111
2147
  cleanupDir(tmpDownloadDir);
@@ -2131,7 +2167,7 @@ const pluginConstructor = (app) => {
2131
2167
  const dlFileName = job.extractedFiles?.[0] ?? job.targetFiles?.[0];
2132
2168
  const dlPath = dlFileName ? path.join(tmpDownloadDir, dlFileName) : null;
2133
2169
  if (!dlPath || !fs.existsSync(dlPath)) {
2134
- removeInstall(chartNumber);
2170
+ rollbackInstall(chartNumber);
2135
2171
  cleanupDir(tmpDownloadDir);
2136
2172
  return;
2137
2173
  }
@@ -2158,7 +2194,7 @@ const pluginConstructor = (app) => {
2158
2194
  }
2159
2195
  catch (convError) {
2160
2196
  app.error(`ShpBasemap conversion failed: ${convError instanceof Error ? convError.message : String(convError)}`);
2161
- removeInstall(chartNumber);
2197
+ rollbackInstall(chartNumber);
2162
2198
  setConvertingState(chartNumber, false);
2163
2199
  if (quarantineDir !== null) {
2164
2200
  cleanupQuarantineDir(quarantineDir);
@@ -2172,7 +2208,7 @@ const pluginConstructor = (app) => {
2172
2208
  }
2173
2209
  downloadManager.removeListener('job-completed', shpListener);
2174
2210
  downloadManager.removeListener('job-failed', shpFailListener);
2175
- removeInstall(chartNumber);
2211
+ rollbackInstall(chartNumber);
2176
2212
  setConvertingState(chartNumber, false);
2177
2213
  cleanupDir(tmpDownloadDir);
2178
2214
  };
@@ -2206,7 +2242,7 @@ const pluginConstructor = (app) => {
2206
2242
  }
2207
2243
  catch (error) {
2208
2244
  app.error(`GSHHG conversion failed: ${error instanceof Error ? error.message : String(error)}`);
2209
- removeInstall(chartNumber);
2245
+ rollbackInstall(chartNumber);
2210
2246
  setConvertingState(chartNumber, false);
2211
2247
  if (quarantineDir !== null) {
2212
2248
  cleanupQuarantineDir(quarantineDir);