fhirsmith 0.16.1 → 0.17.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/tx/problems.js CHANGED
@@ -36,7 +36,7 @@ class ProblemFinder {
36
36
  }
37
37
  let list = await this.unknownVersionsHtml(sourceUnknownVersions, provider, sp);
38
38
  if (list) {
39
- result = result + `<h4>${sp}</h4>` + list;
39
+ result = result + `<h4>${escape(sp)}</h4>` + list;
40
40
  }
41
41
  }
42
42
  }
@@ -51,14 +51,16 @@ class ProblemFinder {
51
51
  entries.sort((a, b) => a[0].localeCompare(b[0]));
52
52
  let html = '<table class="grid"><thead><tr><th>System</th><th>Unknown Versions</th><th>Known Versions</th></tr></thead><tbody>';
53
53
  for (const [system, vset] of entries) {
54
- const systemEsc = escape(system);
54
+ // values from the loaded packages: URL-encoded in links, HTML-escaped everywhere
55
+ const elements = '&_elements=url%2Cversion%2Cname%2Ctitle%2Cstatus%2Ccontent%2Cdate';
55
56
  const versions = [...vset].sort((a, b) => a.localeCompare(b));
56
57
  const versionRefs = [];
57
58
  for (const v of versions) {
58
- versionRefs.push(`<a href="ValueSet?system=${systemEsc}|${escape(v)}&source=${source}&_elements=url%2Cversion%2Cname%2Ctitle%2Cstatus%2Ccontent%2Cdate">${v}</a>`);
59
+ const href = `ValueSet?system=${encodeURIComponent(system + '|' + v)}&source=${encodeURIComponent(source)}${elements}`;
60
+ versionRefs.push(`<a href="${escape(href)}">${escape(v)}</a>`);
59
61
  }
60
- const knownVersions = [...await provider.listCodeSystemVersions(system)].join('<br/>');
61
- html += `<tr><td><a href="CodeSystem?url=${systemEsc}&_elements=url%2Cversion%2Cname%2Ctitle%2Cstatus%2Ccontent%2Cdate">${system}</a></td>`+
62
+ const knownVersions = [...await provider.listCodeSystemVersions(system)].map(v => escape(v)).join('<br/>');
63
+ html += `<tr><td><a href="${escape(`CodeSystem?url=${encodeURIComponent(system)}${elements}`)}">${escape(system)}</a></td>`+
62
64
  `<td>${versionRefs.join('<br/>')}</td><td>${knownVersions}</td></tr>`;
63
65
  }
64
66
  html += '</tbody></table>';
package/tx/provider.js CHANGED
@@ -1,4 +1,5 @@
1
1
  const { CodeSystem } = require("./library/codesystem");
2
+ const { assertNoActiveContent } = require('../library/narrative-check');
2
3
  const {VersionUtilities} = require("../library/version-utilities");
3
4
  const { FhirCodeSystemProvider} = require("./cs/cs-cs");
4
5
  const {OperationContext} = require("./operation-context");
@@ -184,7 +185,10 @@ class Provider {
184
185
  for (const resource of resources) {
185
186
  let cs;
186
187
  try {
187
- cs = new CodeSystem(await contentLoader.loadFile(resource, contentLoader.fhirVersion()));
188
+ const json = await contentLoader.loadFile(resource, contentLoader.fhirVersion());
189
+ // narrative is shown as-is in a page, so it must not carry script (see narrative-check.js)
190
+ assertNoActiveContent(json);
191
+ cs = new CodeSystem(json);
188
192
  } catch (e) {
189
193
  // one unacceptable resource (e.g. contained resources the server doesn't support)
190
194
  // must not stop the rest of the package loading
package/tx/tx-html.js CHANGED
@@ -9,6 +9,7 @@ const htmlServer = require('../library/html-server');
9
9
  const Logger = require('../library/logger');
10
10
  const packageJson = require("../package.json");
11
11
  const escape = require('escape-html');
12
+ const { checkResourceNarratives } = require('../library/narrative-check');
12
13
  const {ExpandWorker} = require("./workers/expand");
13
14
  const ValueSet = require("./library/valueset");
14
15
  const {CodeSystemXML} = require("./xml/codesystem-xml");
@@ -1017,7 +1018,7 @@ class TxHtmlRenderer {
1017
1018
  } else if (_fmt == "html/xml") {
1018
1019
  html += await this.renderResourceXml(json);
1019
1020
  } else if (_fmt == "html/narrative") {
1020
- html += await this.renderResourceWithNarrative(json, json.text?.div);
1021
+ html += await this.renderOriginalNarrative(json);
1021
1022
  } else if (_fmt == "html/ops") {
1022
1023
  html += await this.liquid.renderFile('codesystem-operations', {
1023
1024
  opsId: this.generateResourceId(),
@@ -1040,9 +1041,10 @@ class TxHtmlRenderer {
1040
1041
 
1041
1042
  tab(b, name, rtype, type, id) {
1042
1043
  if (b) {
1043
- return `<li class="active"><a href="#">${name}</a></li>`;
1044
+ return `<li class="active"><a href="#">${escape(name)}</a></li>`;
1044
1045
  } else {
1045
- return `<li><a href="${this.path}/${rtype}/${id}?_format=${type}">${name}</a></li>`;
1046
+ const href = `${this.path}/${encodeURIComponent(rtype)}/${encodeURIComponent(id)}?_format=${encodeURIComponent(type)}`;
1047
+ return `<li><a href="${escape(href)}">${escape(name)}</a></li>`;
1046
1048
  }
1047
1049
  }
1048
1050
  /**
@@ -1078,7 +1080,7 @@ class TxHtmlRenderer {
1078
1080
  } else if (_fmt == "html/xml") {
1079
1081
  html += await this.renderResourceXml(json);
1080
1082
  } else if (_fmt == "html/narrative") {
1081
- html += await this.renderResourceWithNarrative(json, json.text?.div);
1083
+ html += await this.renderOriginalNarrative(json);
1082
1084
  } else if (_fmt == "html/ops") {
1083
1085
  html += await this.liquid.renderFile('valueset-operations', {
1084
1086
  opsId: this.generateResourceId(),
@@ -1218,7 +1220,7 @@ class TxHtmlRenderer {
1218
1220
  } else if (_fmt == "html/json") {
1219
1221
  html += await this.renderResourceJson(json);
1220
1222
  } else if (_fmt == "html/narrative") {
1221
- html += await this.renderResourceWithNarrative(json, json.text?.div);
1223
+ html += await this.renderOriginalNarrative(json);
1222
1224
  } else if (_fmt == "html/ops") {
1223
1225
  const sourceSet = new Set();
1224
1226
  const codeSet = new Set();
@@ -1725,7 +1727,7 @@ class TxHtmlRenderer {
1725
1727
  html += '<div class="card-header">Bundle</div>';
1726
1728
  html += '<div class="card-body">';
1727
1729
  html += `<p><strong>Type:</strong> ${escape(json.type)}</p>`;
1728
- html += `<p><strong>Total:</strong> ${json.total || 'N/A'}</p>`;
1730
+ html += `<p><strong>Total:</strong> ${escape(String(json.total ?? 'N/A'))}</p>`;
1729
1731
  html += '</div>';
1730
1732
  html += '</div>';
1731
1733
 
@@ -1826,6 +1828,18 @@ class TxHtmlRenderer {
1826
1828
  return html;
1827
1829
  }
1828
1830
 
1831
+ /**
1832
+ * The resource's own narrative, as it is - unless it carries active content, which is
1833
+ * refused at load time but may reach here from a source that isn't loaded that way
1834
+ */
1835
+ async renderOriginalNarrative(json) {
1836
+ const found = checkResourceNarratives({ resourceType: json.resourceType, id: json.id, text: json.text });
1837
+ if (found) {
1838
+ return `<div class="alert alert-danger">This narrative is not shown: ${escape(found)}</div>`;
1839
+ }
1840
+ return this.renderResourceWithNarrative(json, json.text?.div);
1841
+ }
1842
+
1829
1843
  async renderResourceJson(json) {
1830
1844
  let html = "";
1831
1845
  html += `<div class="json-content" style="margin-top: 10px;">`;
@@ -1864,7 +1878,8 @@ class TxHtmlRenderer {
1864
1878
  return await this.liquid.renderFile('operations-form', {
1865
1879
  vcSystemId,
1866
1880
  inferSystemId,
1867
- valueSetsJson: JSON.stringify(json.valueSets || [])
1881
+ // inside a <script>: no '<', so no value can close it
1882
+ valueSetsJson: JSON.stringify(json.valueSets || []).replace(/</g, '\\u003c')
1868
1883
  });
1869
1884
  }
1870
1885
 
package/tx/tx.js CHANGED
@@ -211,7 +211,7 @@ class TXModule {
211
211
  }
212
212
  };
213
213
  void prune(); // errors are logged inside
214
- this.timers.push(setInterval(prune, 24 * 60 * 60 * 1000));
214
+ this.timers.push(setInterval(() => void prune(), 24 * 60 * 60 * 1000));
215
215
  }
216
216
  }
217
217
 
@@ -305,13 +305,14 @@ class TXModule {
305
305
  if (this.stats) {
306
306
  this.stats.addTask("Client Cache", "5 min");
307
307
  }
308
- this.timers.push(setInterval(async () => {
308
+ const updateCodeSystemList = async () => {
309
309
  try {
310
310
  await endpointInfo.provider.updateCodeSystemList();
311
311
  } catch (error) {
312
312
  this.log.error(`Error updating CodeSystem list for ${endpointPath}: ${error.message}`);
313
313
  }
314
- }, 60 * 1000));
314
+ };
315
+ this.timers.push(setInterval(() => void updateCodeSystemList(), 60 * 1000)); // errors are logged inside
315
316
  this.log.info(`CodeSystem list update scheduled for ${endpointPath}`);
316
317
  this.timers.push(setInterval(() => {
317
318
  endpointInfo.resourceCache.prune(cacheTimeoutMs);
@@ -4,6 +4,9 @@ const sqlite3 = require('sqlite3').verbose();
4
4
  const { VersionUtilities } = require('../../library/version-utilities');
5
5
  const ValueSet = require("../library/valueset");
6
6
 
7
+ // Settings key written once a package cache has been fully populated (see isComplete)
8
+ const CACHE_COMPLETE_KEY = 'cache-complete';
9
+
7
10
  // Columns that can be returned directly without parsing JSON
8
11
  const INDEXED_COLUMNS = ['id', 'url', 'version', 'date', 'description', 'name', 'publisher', 'status', 'title'];
9
12
 
@@ -485,6 +488,29 @@ class ValueSetDatabase {
485
488
  });
486
489
  }
487
490
 
491
+ /**
492
+ * True if this cache was fully populated: markComplete() was reached. A cache
493
+ * whose population was cut short (the process killed part way through) has no
494
+ * marker - and so does a file too damaged to open or read - so it reports false.
495
+ * @returns {Promise<boolean>}
496
+ */
497
+ async isComplete() {
498
+ try {
499
+ return (await this.getSetting(CACHE_COMPLETE_KEY)) != null;
500
+ } catch (e) {
501
+ return false;
502
+ }
503
+ }
504
+
505
+ /**
506
+ * Record that this cache has been fully populated. Called once, after the last
507
+ * value set has been written
508
+ * @returns {Promise<void>}
509
+ */
510
+ async markComplete() {
511
+ await this.setSetting(CACHE_COMPLETE_KEY, new Date().toISOString());
512
+ }
513
+
488
514
  /**
489
515
  * Insert or update a single ValueSet in the database
490
516
  * @param {Object} valueSet - The ValueSet resource
@@ -1,4 +1,5 @@
1
1
  const path = require('path');
2
+ const { assertNoActiveContent } = require('../../library/narrative-check');
2
3
  const fs = require('fs').promises;
3
4
  const { AbstractValueSetProvider } = require('./vs-api');
4
5
  const { PackageContentLoader } = require('../../library/package-manager');
@@ -21,7 +22,9 @@ class PackageValueSetProvider extends AbstractValueSetProvider {
21
22
  super();
22
23
  validateParameter(packageLoader, "packageLoader", PackageContentLoader);
23
24
  this.packageLoader = packageLoader;
24
- this.dbPath = path.join(packageLoader.packageFolder, '.valuesets.db');
25
+ // -2: caches from before value sets were checked for active content in their narrative
26
+ // are rebuilt (see initialize)
27
+ this.dbPath = path.join(packageLoader.packageFolder, '.valuesets-2.db');
25
28
  this.database = new ValueSetDatabase(this.dbPath);
26
29
  this.valueSetMap = new Map();
27
30
  this.initialized = false;
@@ -45,12 +48,21 @@ class PackageValueSetProvider extends AbstractValueSetProvider {
45
48
  return;
46
49
  }
47
50
  await this.packageLoader.initialize();
48
-
49
- const dbExists = await this.database.exists();
51
+ await fs.rm(path.join(this.packageLoader.packageFolder, '.valuesets.db'), { force: true }); // the old cache
52
+
53
+ // A cache is only used once its population finished. One that was cut short
54
+ // (the process killed part way through) would otherwise be trusted forever,
55
+ // silently missing whatever hadn't been written yet
56
+ let dbExists = await this.database.exists();
57
+ if (dbExists && !(await this.database.isComplete())) {
58
+ require('../../library/logger').getInstance().child({ module: 'tx' })
59
+ .warn(`Package ${this.packageLoader.pid ? this.packageLoader.pid() : ''}: value set cache ${this.dbPath} is incomplete - rebuilding it`);
60
+ await this.#discardDatabase();
61
+ dbExists = false;
62
+ }
50
63
 
51
64
  if (!dbExists) {
52
- await this.database.create();
53
- await this._populateDatabase();
65
+ await this.#buildDatabase();
54
66
  }
55
67
 
56
68
  this.valueSetMap = await this.database.loadAllValueSets(this.sourcePackage());
@@ -60,11 +72,8 @@ class PackageValueSetProvider extends AbstractValueSetProvider {
60
72
  // package-manager.js): if any cached value set still carries a "system|version"
61
73
  // include or a pipe in its url, rebuild this cache from the package.
62
74
  if (dbExists && this.#hasPipeVersionedCanonicals()) {
63
- await this.database.close();
64
- await fs.rm(this.dbPath, { force: true });
65
- this.database = new ValueSetDatabase(this.dbPath);
66
- await this.database.create();
67
- await this._populateDatabase();
75
+ await this.#discardDatabase();
76
+ await this.#buildDatabase();
68
77
  this.valueSetMap = await this.database.loadAllValueSets(this.sourcePackage());
69
78
  }
70
79
 
@@ -84,6 +93,27 @@ class PackageValueSetProvider extends AbstractValueSetProvider {
84
93
  this.initialized = true;
85
94
  }
86
95
 
96
+ /**
97
+ * Create the cache and fill it from the package. The complete marker is written
98
+ * last, so a build that doesn't finish is detected (and redone) next time
99
+ * @private
100
+ */
101
+ async #buildDatabase() {
102
+ await this.database.create();
103
+ await this._populateDatabase();
104
+ await this.database.markComplete();
105
+ }
106
+
107
+ /**
108
+ * Close and delete the cache file, leaving a fresh (not yet created) database
109
+ * @private
110
+ */
111
+ async #discardDatabase() {
112
+ await this.database.close();
113
+ await fs.rm(this.dbPath, { force: true });
114
+ this.database = new ValueSetDatabase(this.dbPath);
115
+ }
116
+
87
117
  /**
88
118
  * True if any loaded value set still has a pipe-versioned canonical (url or
89
119
  * compose include/exclude system) - the signature of a cache written before
@@ -134,6 +164,8 @@ class PackageValueSetProvider extends AbstractValueSetProvider {
134
164
  // don't store a value set the server can't use (e.g. one that contains a
135
165
  // CodeSystem): it would be listed by search, and fail when read
136
166
  checkContained(valueSet);
167
+ // its narrative is shown as-is in a page, so it must not carry script
168
+ assertNoActiveContent(valueSet);
137
169
  } catch (e) {
138
170
  require('../../library/logger').getInstance().child({ module: 'tx' })
139
171
  .warn(`Package ${this.packageLoader.pid ? this.packageLoader.pid() : ''}: skipping ValueSet ${valueSet.url}: ${e.message}`);
@@ -836,7 +836,7 @@ class ValueSetExpander {
836
836
  notClosed.value = true;
837
837
  }
838
838
  const prep = await cs.getPrepContext(true);
839
- const ctxt = await cs.searchFilter(prep, filter, false);
839
+ const ctxt = await cs.searchFilter(prep, filter, true);
840
840
  let set = await cs.executeFilters(prep);
841
841
  if (this.count === 0 && valueSets.length === 0) {
842
842
  // count=0 (count only): report the filtered size directly without
@@ -472,7 +472,12 @@ class MetadataHandler {
472
472
  { name: 'useSupplement' },
473
473
  { name: 'valueSet' },
474
474
  { name: 'valueSetVersion' }
475
- ]
475
+ ],
476
+ textFilter: 'On code systems loaded from files and packages, a text filter ignores case, and accents on Latin, Greek and Cyrillic letters. ' +
477
+ 'It matches concepts whose code, display, definition or a designation contains the text. ' +
478
+ 'A filter of several words also matches a display or designation in which each word starts a word, ' +
479
+ 'in any order; for this, punctuation and spaces only separate words. The closest matches come first. ' +
480
+ 'Other code systems, such as SNOMED CT, LOINC and RxNorm, match text in their own way.'
476
481
  };
477
482
  }
478
483
 
@@ -11,7 +11,7 @@
11
11
  *
12
12
  * node utilities/submit-test-report.js [-server url] [-file path] [-token token] [-header name]
13
13
  *
14
- * -server the /testing base; default https://testing.fhir.org/testing
14
+ * -server the /testing base; default https://npm.fhir.org/testing
15
15
  * -file default test-cases-report.json in the FHIRsmith root
16
16
  * -token if the server requires one; or set FHIRSMITH_TESTING_TOKEN
17
17
  * -header the header the token goes in; default Authorization (sent as "Bearer <token>")
package/xig/xig.js CHANGED
@@ -19,12 +19,14 @@ const escape = require('escape-html');
19
19
 
20
20
  const Logger = require('../library/logger');
21
21
  const {describeCron} = require("../library/cron-utilities");
22
+ const {tokenMatches, tokenConfigured} = require('../library/request-token');
22
23
  const xigLog = Logger.getInstance().child({ module: 'xig' });
23
24
 
24
25
  const router = express.Router();
25
26
 
26
27
  // Configuration
27
- const XIG_DB_URL = 'http://fhir.org/guides/stats/xig.db';
28
+ // https: the database is trusted content (its HTML is shown as is), so it must come from fhir.org
29
+ const XIG_DB_URL = 'https://fhir.org/guides/stats/xig.db';
28
30
  const XIG_DB_PATH = folders.filePath('xig', 'xig.db');
29
31
  const TEMPLATE_PATH = path.join(__dirname, 'xig-template.html');
30
32
 
@@ -1718,6 +1720,11 @@ function downloadFile(url, destination, maxRedirects = 5) {
1718
1720
  }
1719
1721
  }
1720
1722
 
1723
+ // never be redirected off https
1724
+ if (new URL(redirectUrl).protocol !== 'https:') {
1725
+ reject(Object.assign(new Error(`Refused a redirect to ${redirectUrl}: the database is only downloaded over https`), { downloadMeta }));
1726
+ return;
1727
+ }
1721
1728
  attemptDownload(redirectUrl, redirectCount + 1);
1722
1729
  return;
1723
1730
  }
@@ -2203,6 +2210,23 @@ function getDatabaseAgeInfo() {
2203
2210
  };
2204
2211
  }
2205
2212
 
2213
+ // The form on /xig/stats that starts an update (POST /xig/update), when an updateToken is configured
2214
+ function buildUpdateForm() {
2215
+ if (!tokenConfigured(xigSettings.updateToken)) {
2216
+ return '<p class="text-muted">An update can be started from here when an updateToken is set in the xig configuration.</p>';
2217
+ }
2218
+ let html = '<h4>Update the database now</h4>';
2219
+ html += '<form method="POST" action="/xig/update" class="mb-4">';
2220
+ html += '<p>Downloads the current database from fhir.org (about 3 GB) and switches to it. ';
2221
+ html += updateInProgress ? '<strong>An update is running now.</strong>' : 'It takes several minutes.';
2222
+ html += '</p>';
2223
+ html += '<label for="xig-update-token">Update token</label> ';
2224
+ html += '<input type="password" id="xig-update-token" name="token" required autocomplete="off" maxlength="200"> ';
2225
+ html += '<button type="submit" class="btn btn-primary">Start the update</button>';
2226
+ html += '</form>';
2227
+ return html;
2228
+ }
2229
+
2206
2230
  function buildStatsTable(statsData) {
2207
2231
  let html = '<table class="table table-striped table-bordered">';
2208
2232
  html += '<thead class="table-dark">';
@@ -2429,16 +2453,25 @@ router.get('/', async (req, res) => {
2429
2453
  const title = 'FHIR Resources';
2430
2454
 
2431
2455
  // Parse query parameters
2456
+ // Each a string: a repeated parameter (?text=a&text=b) arrives as an array, which
2457
+ // the filters and SQL builders would fail on. Capped in length
2458
+ const qp = (name, dflt = '') => {
2459
+ let v = req.query[name];
2460
+ if (Array.isArray(v)) {
2461
+ v = v[0];
2462
+ }
2463
+ return typeof v === 'string' && v !== '' ? v.substring(0, 200) : dflt;
2464
+ };
2432
2465
  const queryParams = {
2433
- ver: req.query.ver || '',
2434
- auth: req.query.auth || '',
2435
- realm: req.query.realm || '',
2436
- type: req.query.type || '',
2437
- rt: req.query.rt || '',
2438
- text: req.query.text || '',
2439
- pkg: req.query.pkg || '',
2440
- onlyUsed: req.query.onlyUsed || '',
2441
- offset: req.query.offset || '0'
2466
+ ver: qp('ver'),
2467
+ auth: qp('auth'),
2468
+ realm: qp('realm'),
2469
+ type: qp('type'),
2470
+ rt: qp('rt'),
2471
+ text: qp('text'),
2472
+ pkg: qp('pkg'),
2473
+ onlyUsed: qp('onlyUsed'),
2474
+ offset: qp('offset', '0')
2442
2475
  };
2443
2476
 
2444
2477
  // Parse offset for pagination
@@ -2600,7 +2633,7 @@ router.get('/stats', async (req, res) => {
2600
2633
  introContent += `</div>`;
2601
2634
  }
2602
2635
 
2603
- const fullContent = introContent + content;
2636
+ const fullContent = introContent + content + buildUpdateForm();
2604
2637
 
2605
2638
  const stats = await gatherPageStatistics();
2606
2639
  stats.processingTime = Date.now() - startTime;
@@ -3125,8 +3158,44 @@ router.get('/cache', async (req, res) => {
3125
3158
  }
3126
3159
  });
3127
3160
 
3128
- router.post('/update', async (req, res) => {
3161
+ // POST /xig/update re-downloads the database (about 3 GB), so it needs the updateToken from
3162
+ // the xig config: in the x-update-token header, or (from the form on /xig/stats) in a
3163
+ // "token" form field. With no updateToken it is disabled.
3164
+ //
3165
+ // The form gets a page back straight away, while the update runs on (it takes minutes);
3166
+ // /xig/stats shows how it went. Other callers wait for the update and get JSON.
3167
+ router.post('/update', express.urlencoded({ extended: false, limit: '2kb' }), async (req, res) => {
3168
+ const fromForm = !!req.is('application/x-www-form-urlencoded');
3169
+ const page = (status, alert, message) => {
3170
+ const content = `<div class="alert alert-${alert}">${escape(message)}</div><p><a href="/xig/stats">Back to the status page</a></p>`;
3171
+ res.status(status).setHeader('Content-Type', 'text/html');
3172
+ res.send(renderPage('Update the XIG database', content));
3173
+ };
3174
+ const refuse = (message) => fromForm ? page(403, 'danger', message) : res.status(403).json({ status: 'ERROR', error: 'forbidden: ' + message });
3175
+
3129
3176
  try {
3177
+ if (!tokenConfigured(xigSettings.updateToken)) {
3178
+ refuse('no updateToken is configured, so this endpoint is disabled');
3179
+ return;
3180
+ }
3181
+ const supplied = req.headers['x-update-token'] ?? req.body?.token;
3182
+ if (!tokenMatches(xigSettings.updateToken, supplied)) {
3183
+ xigLog.warn('Rejected /xig/update: missing or invalid token (from ' + req.ip + ')');
3184
+ refuse('missing or invalid x-update-token');
3185
+ return;
3186
+ }
3187
+
3188
+ if (fromForm) {
3189
+ if (updateInProgress) {
3190
+ page(200, 'info', 'An update is already running. The status page shows how it is going.');
3191
+ return;
3192
+ }
3193
+ xigLog.info('Manual update triggered from the status page');
3194
+ updateXigDatabase().catch((err) => xigLog.error('XIG database update failed: ' + err.message));
3195
+ page(202, 'success', 'The update has started. It takes several minutes; the status page shows how it is going.');
3196
+ return;
3197
+ }
3198
+
3130
3199
  xigLog.info('Manual update triggered via API');
3131
3200
  await updateXigDatabase();
3132
3201
  res.json({
@@ -3143,10 +3212,16 @@ router.post('/update', async (req, res) => {
3143
3212
  });
3144
3213
 
3145
3214
  let globalStats;
3215
+ let xigSettings = {};
3216
+ // The xig section of the config (initializeXigModule; tests set it directly)
3217
+ function setXigSettings(xigConfig) {
3218
+ xigSettings = xigConfig || {};
3219
+ }
3146
3220
  // Initialize the XIG module
3147
3221
  async function initializeXigModule(stats, xigConfig) {
3148
3222
  try {
3149
3223
  globalStats = stats;
3224
+ setXigSettings(xigConfig);
3150
3225
  loadTemplate();
3151
3226
 
3152
3227
  await initializeDatabase();
@@ -3196,6 +3271,7 @@ function shutdown() {
3196
3271
  // Export everything
3197
3272
  module.exports = {
3198
3273
  router,
3274
+ setXigSettings,
3199
3275
  updateXigDatabase,
3200
3276
  getDatabaseInfo,
3201
3277
  shutdown,
package/gr.sh DELETED
@@ -1,3 +0,0 @@
1
- #!/usr/bin/env bash
2
- git tag v$1
3
- git push origin v$1