fhirsmith 0.7.6 → 0.8.2

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 (57) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/README.md +5 -1
  3. package/library/languages.js +10 -0
  4. package/package.json +1 -1
  5. package/packages/package-crawler.js +2 -2
  6. package/publisher/publisher.js +1 -1
  7. package/registry/registry.js +2 -2
  8. package/root-bare-template.html +1 -2
  9. package/security.md +3 -0
  10. package/server.js +100 -70
  11. package/stats.js +37 -6
  12. package/tx/cs/cs-api.js +8 -4
  13. package/tx/cs/cs-loinc.js +14 -2
  14. package/tx/cs/cs-omop.js +5 -3
  15. package/tx/cs/cs-rxnorm.js +18 -16
  16. package/tx/cs/cs-snomed.js +279 -6
  17. package/tx/data/cpt-fragment.db +0 -0
  18. package/tx/data/cs-de.json +186 -0
  19. package/tx/data/cs-extensions.json +92 -0
  20. package/tx/data/cs-simple.json +130 -0
  21. package/tx/data/cs-supplement.json +78 -0
  22. package/tx/data/lang.dat +49180 -0
  23. package/tx/data/languages.csv +191 -0
  24. package/tx/data/loinc-subset.txt +75 -0
  25. package/tx/data/omop-fragment.db +0 -0
  26. package/tx/data/readme.md +43 -0
  27. package/tx/data/regions.csv +273 -0
  28. package/tx/data/rxnorm-subset.txt +22 -0
  29. package/tx/data/snomed-subset.txt +47 -0
  30. package/tx/data/ucum-essence.xml +2059 -0
  31. package/tx/html/dash-metrics.liquid +147 -0
  32. package/tx/importers/import-rxnorm.module.js +4 -30
  33. package/tx/library/canonical-resource.js +8 -0
  34. package/tx/library/conceptmap.js +29 -1
  35. package/tx/library/designations.js +4 -8
  36. package/tx/library/extensions.js +4 -3
  37. package/tx/library/renderer.js +9 -9
  38. package/tx/ocl/cm-ocl.cjs +185 -65
  39. package/tx/ocl/cs-ocl.cjs +69 -50
  40. package/tx/ocl/jobs/background-queue.cjs +0 -8
  41. package/tx/ocl/mappers/concept-mapper.cjs +13 -3
  42. package/tx/ocl/shared/patches.cjs +1 -0
  43. package/tx/ocl/vs-ocl.cjs +137 -157
  44. package/tx/operation-context.js +3 -3
  45. package/tx/params.js +2 -2
  46. package/tx/provider.js +6 -3
  47. package/tx/sct/structures.js +6 -1
  48. package/tx/tx.fhir.org.yml +1 -1
  49. package/tx/vs/vs-database.js +107 -23
  50. package/tx/vs/vs-vsac.js +66 -19
  51. package/tx/workers/expand.js +10 -10
  52. package/tx/workers/related.js +2 -2
  53. package/tx/workers/search.js +2 -1
  54. package/tx/workers/translate.js +222 -33
  55. package/tx/workers/validate.js +13 -13
  56. package/tx/xversion/xv-parameters.js +54 -1
  57. package/xig/xig.js +171 -9
package/CHANGELOG.md CHANGED
@@ -5,6 +5,54 @@ All notable changes to the Health Intersections Node Server will be documented i
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [v0.8.2] - 2026-03-29
9
+
10
+ ### Added
11
+
12
+ - Support for implicit snomed concept maps
13
+
14
+ ### Changed
15
+
16
+ - Reverse the [interpretation of RxNorm [rel] and [rela] value sets](https://chat.fhir.org/#narrow/channel/179202-terminology/topic/Inverted.20query.20for.20RELA.20in.20using.20RxNorm.20page/with/582270767)
17
+ - Improve modifier extension message
18
+ -
19
+ ### Fixed
20
+
21
+ - fix missing files from npm package
22
+ - Add missing styles to dashboard
23
+ - $translate fixes: don't return duplicate matches, handle R4/R5 issues properly, fix missed comments and products
24
+ - fix handling force-value-set version parameter
25
+
26
+ ### Tx Conformance Statement
27
+
28
+ FHIRsmith passed all 1498 HL7 terminology service tests (modes tx.fhir.org+omop+general+snomed, tests v1.9.1, runner v6.9.4
29
+
30
+ ## [v0.8.0] - 2026-03-27
31
+
32
+ ### Added
33
+
34
+ - XIG: add JSON and CSV downloads
35
+ - TX: Add snomed filter support for inactive, moduleId, and properties
36
+
37
+ ### Changed
38
+
39
+ - Improve Dashboard Presentation
40
+ - Make docker image platform compatible with apple silicon (arm)
41
+ - TX: update rxnorm version for tx.fhir.org
42
+ - TX: Improve VSAC information page
43
+
44
+ ### Fixed
45
+
46
+ - XIG: fix valueset source filter
47
+ - TX: Fix bug in language processing looking up country codes
48
+ - TX: Fix up terminology search for LOINC and generally
49
+ - TX: fix rxnorm property support and search performance
50
+ - Publisher: fix status display when building draft IG
51
+
52
+ ### Tx Conformance Statement
53
+
54
+ FHIRsmith passed all 1498 HL7 terminology service tests (modes tx.fhir.org+omop+general+snomed, tests v1.9.1, runner v6.9.4)
55
+
8
56
  ## [v0.7.6] - 2026-03-25
9
57
 
10
58
  ### Added
package/README.md CHANGED
@@ -10,7 +10,7 @@ This server provides a set of server-side services that are useful for the FHIR
10
10
 
11
11
  ## Services useful the community as a whole
12
12
 
13
- * [TX Registry](registry/readme.md) - **Terminology System Registry** as [described by the terminology ecosystem specification](https://build.fhir.org/ig/HL7/fhir-tx-ecosystem-ig)(as running at http://tx.fhir.org/tx-reg)
13
+ * [TX Registry](registry/readme.md) - **Terminology System Registry** as [described by the terminology ecosystem specification](https://build.fhir.org/ig/HL7/fhir-tx-ecosystem-ig) (as running at http://tx.fhir.org/tx-reg)
14
14
  * [Package server](packages/readme.md) - **NPM-style FHIR package registry** with search, versioning, and downloads, consistent with the FHIR NPM Specification (as running at http://packages2.fhir.org/packages)
15
15
  * [XIG server](xig/readme.md) - **Comprehensive FHIR IG analytics** with resource breakdowns by version, authority, and realm (as running at http://packages2.fhir.org/packages)
16
16
  * [Publisher](publisher/readme.md) - FHIR publishing services (coming)
@@ -43,6 +43,10 @@ There are 4 executable programs:
43
43
 
44
44
  Unless you're developing, you only need the first two
45
45
 
46
+ FHIRsmith is open source - see below, and you're welcome to use it for any kind of use. Note,
47
+ though, that if you support FHIRsmith commercially as part of a managed service or product, you
48
+ are required to be a Commercial Partner of HL7 - see (link to be provided).
49
+
46
50
  ### Quick Start
47
51
 
48
52
  * Install FHIRSmith (using docker, or an NPM release, or just get the code by git)
@@ -455,6 +455,16 @@ class Languages {
455
455
  return true;
456
456
  }
457
457
 
458
+ includesLanguage(code) {
459
+ const llang = new Language(code);
460
+ for (const lang of this.languages) {
461
+ if (lang.matches(llang)) {
462
+ return true;
463
+ }
464
+ }
465
+ return false;
466
+ }
467
+
458
468
  /**
459
469
  * Convert to string representation (similar to Accept-Language header format)
460
470
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fhirsmith",
3
- "version": "0.7.6",
3
+ "version": "0.8.2",
4
4
  "description": "A Node.js server that provides a collection of tools to serve the FHIR ecosystem",
5
5
  "main": "server.js",
6
6
  "engines": {
@@ -120,7 +120,7 @@ class PackageCrawler {
120
120
  this.log.info(`Web crawler completed successfully in ${runTime}ms`);
121
121
  this.log.info(`Total bytes processed: ${this.totalBytes}`);
122
122
 
123
- this.stats.task('Package Crawler', 'Complete');
123
+ this.stats.taskDone('Package Crawler', 'Complete');
124
124
  return this.crawlerLog;
125
125
 
126
126
  } catch (error) {
@@ -128,7 +128,7 @@ class PackageCrawler {
128
128
  this.crawlerLog.runTime = `${runTime}ms`;
129
129
  this.crawlerLog.fatalException = error.message;
130
130
  this.crawlerLog.endTime = new Date().toISOString();
131
- this.stats.task('Package Crawler', 'Error: '+error.message);
131
+ this.stats.taskError('Package Crawler', 'Error: '+error.message);
132
132
 
133
133
  this.log.error('Web crawler failed: '+ error);
134
134
  throw error;
@@ -481,7 +481,7 @@ class PublisherModule {
481
481
 
482
482
  // Record the log file path and local folder immediately so they're accessible
483
483
  // even if the build fails later
484
- await this.updateTaskStatus(task.id, task.status, {
484
+ await this.updateTaskStatus(task.id, 'building', {
485
485
  build_output_path: logFile,
486
486
  local_folder: taskDir
487
487
  });
@@ -163,10 +163,10 @@ class RegistryModule {
163
163
  `Found ${newData.registries.length} registries, ` +
164
164
  `${metadata.errors.length} errors, ` +
165
165
  `downloaded ${this.crawler.formatBytes(metadata.totalBytes)}`);
166
- this.stats.task('TxRegistry', 'Crawling Finished');
166
+ this.stats.taskDone('TxRegistry', 'Crawling Finished');
167
167
  } catch (error) {
168
168
  this.logger.error('Crawl failed:', error);
169
- this.stats.task('TxRegistry', 'Crawling Error: '+error.message);
169
+ this.stats.taskError('TxRegistry', 'Crawling Error: '+error.message);
170
170
  } finally {
171
171
  this.crawlInProgress = false;
172
172
  }
@@ -57,11 +57,10 @@
57
57
  <div class="inner-wrapper">
58
58
  <div class="col-9">
59
59
 
60
- <h2><img border="0" src="/FHIRsmith32.png" style="vertical-align: text-bottom"/> FHIRsmith: [%title%] </h2>
60
+ <h2>[%title%] </h2>
61
61
 
62
62
  [%content%]
63
63
 
64
- [%about%]
65
64
 
66
65
  </div>
67
66
 
package/security.md CHANGED
@@ -29,4 +29,7 @@ A typical NGINX configuration would be:
29
29
  limit_conn perip 50;
30
30
  limit_conn perserver 500;
31
31
  ```
32
+ ## SSL
33
+
34
+ This server doesn't provide SSL support - use an NGINX reverse proxy for that.
32
35
 
package/server.js CHANGED
@@ -267,60 +267,8 @@ async function loadTemplates() {
267
267
  }
268
268
  }
269
269
 
270
- async function buildDashboardContent() {
271
- // Calculate uptime
272
- const uptimeMs = Date.now() - stats.startTime;
273
- const uptimeSeconds = Math.floor(uptimeMs / 1000);
274
- const uptimeDays = Math.floor(uptimeSeconds / 86400);
275
- const uptimeHours = Math.floor((uptimeSeconds % 86400) / 3600);
276
- const uptimeMinutes = Math.floor((uptimeSeconds % 3600) / 60);
277
- const uptimeSecs = uptimeSeconds % 60;
278
- let uptimeStr = '';
279
- if (uptimeDays > 0) uptimeStr += `${uptimeDays}d `;
280
- if (uptimeHours > 0 || uptimeDays > 0) uptimeStr += `${uptimeHours}h `;
281
- if (uptimeMinutes > 0 || uptimeHours > 0 || uptimeDays > 0) uptimeStr += `${uptimeMinutes}m `;
282
- uptimeStr += `${uptimeSecs}s`;
283
-
284
- // Memory usage
285
- const memUsage = process.memoryUsage();
286
- const heapUsedMB = (memUsage.heapUsed / 1024 / 1024).toFixed(2);
287
- const heapAvailableMB = ((memUsage.heapTotal - memUsage.heapUsed) / 1024 / 1024).toFixed(2);
288
- const rssMB = (memUsage.rss / 1024 / 1024).toFixed(2);
289
- const freeMemMB = (os.freemem() / 1024 / 1024).toFixed(0);
290
- const totalMemMB = (os.totalmem() / 1024 / 1024).toFixed(0);
291
-
292
- let content = '';
293
- content += '<table class="grid">';
294
- content += '<tr>';
295
- content += `<td><strong>Uptime:</strong> ${escape(uptimeStr)}</td>`;
296
- content += `<td><strong>Request Count:</strong> ${stats.requestCount} (static: ${stats.staticRequestCount})</td>`;
297
- content += `<td><strong>Free Memory:</strong> ${freeMemMB} MB of ${totalMemMB} MB</td>`;
298
- content += '</tr>';
299
- content += '<tr>';
300
- content += `<td><strong>Heap Used:</strong> ${heapUsedMB} MB</td>`;
301
- content += `<td><strong>Heap Available:</strong> ${heapAvailableMB} MB</td>`;
302
- content += `<td><strong>Process Memory:</strong> ${rssMB} MB</td>`;
303
- content += '</tr>';
304
- content += getLogStats();
305
- content += '</table>';
306
-
307
- // ===== Metrics Graphs =====
308
- const liquid = new Liquid({
309
- root: path.join(__dirname, 'tx', 'html'),
310
- extname: '.liquid'
311
- });
312
- content += await liquid.renderFile('home-metrics', {
313
- historyJson: JSON.stringify(stats.history),
314
- startTime: stats.startTime
315
- });
316
- content += stats.taskDetails();
317
-
318
- return content;
319
- }
320
-
321
270
  async function buildRootPageContent() {
322
271
  stats.requestCount++;
323
- let mc = 0;
324
272
  let content = '<div class="row mb-4">';
325
273
  content += '<div class="col-12">';
326
274
 
@@ -329,35 +277,30 @@ async function buildRootPageContent() {
329
277
 
330
278
  // Check which modules are enabled and add them to the list
331
279
  if (config.modules.packages.enabled) {
332
- mc++;
333
280
  content += '<li class="list-group-item">';
334
281
  content += '<a href="/packages" class="text-decoration-none">Package Server</a>: Browse and download FHIR Implementation Guide packages';
335
282
  content += '</li>';
336
283
  }
337
284
 
338
285
  if (config.modules.xig.enabled) {
339
- mc++;
340
286
  content += '<li class="list-group-item">';
341
287
  content += '<a href="/xig" class="text-decoration-none">FHIR IG Statistics</a>: Statistics and analysis of FHIR Implementation Guides';
342
288
  content += '</li>';
343
289
  }
344
290
 
345
291
  if (config.modules.shl.enabled) {
346
- mc++;
347
292
  content += '<li class="list-group-item">';
348
293
  content += '<a href="/shl" class="text-decoration-none">SHL Server</a>: SMART Health Links management and validation';
349
294
  content += '</li>';
350
295
  }
351
296
 
352
297
  if (config.modules.vcl.enabled) {
353
- mc++;
354
298
  content += '<li class="list-group-item">';
355
299
  content += '<a href="/VCL" class="text-decoration-none">VCL Server</a>: ValueSet Compose Language expression parsing';
356
300
  content += '</li>';
357
301
  }
358
302
 
359
303
  if (config.modules.registry && config.modules.registry.enabled) {
360
- mc++;
361
304
  content += '<li class="list-group-item">';
362
305
  content += '<a href="/tx-reg" class="text-decoration-none">Terminology Server Registry</a>: ';
363
306
  content += 'Discover and query FHIR terminology servers for code system and value set support';
@@ -365,7 +308,6 @@ async function buildRootPageContent() {
365
308
  }
366
309
 
367
310
  if (config.modules.publisher && config.modules.publisher.enabled) {
368
- mc++;
369
311
  content += '<li class="list-group-item">';
370
312
  content += '<a href="/publisher" class="text-decoration-none">FHIR Publisher</a>: ';
371
313
  content += 'Manage FHIR Implementation Guide publication tasks and approvals';
@@ -373,7 +315,6 @@ async function buildRootPageContent() {
373
315
  }
374
316
 
375
317
  if (config.modules.token && config.modules.token.enabled) {
376
- mc++;
377
318
  content += '<li class="list-group-item">';
378
319
  content += '<a href="/token" class="text-decoration-none">Token Server</a>: ';
379
320
  content += 'OAuth authentication and API key management for FHIR services';
@@ -381,7 +322,6 @@ async function buildRootPageContent() {
381
322
  }
382
323
 
383
324
  if (config.modules.npmprojector && config.modules.npmprojector.enabled) {
384
- mc++;
385
325
  content += '<li class="list-group-item">';
386
326
  content += '<a href="/npmprojector" class="text-decoration-none">NpmProjector</a>: ';
387
327
  content += 'Hot-reloading FHIR server with FHIRPath-based search indexes';
@@ -389,7 +329,6 @@ async function buildRootPageContent() {
389
329
  }
390
330
 
391
331
  if (config.modules?.['ext-tracker']?.enabled) {
392
- mc++;
393
332
  content += '<li class="list-group-item">';
394
333
  content += '<a href="/ext-tracker" class="text-decoration-none">Extension Tracker</a>: ';
395
334
  content += 'View of Extension Usage';
@@ -404,7 +343,6 @@ async function buildRootPageContent() {
404
343
  content += '<ul class="mt-2 mb-0">';
405
344
  for (const fc of folders) {
406
345
  if (fc.enabled === false) continue;
407
- mc++;
408
346
  content += '<li>';
409
347
  content += `<a href="${fc.url}" class="text-decoration-none">${fc.name}</a>: `;
410
348
  content += 'File folder with write control';
@@ -421,7 +359,6 @@ async function buildRootPageContent() {
421
359
  if (config.modules.tx.endpoints && config.modules.tx.endpoints.length > 0) {
422
360
  content += '<ul class="mt-2 mb-0">';
423
361
  for (const endpoint of config.modules.tx.endpoints) {
424
- mc++;
425
362
  content += `<li><a href="${endpoint.path}" class="text-decoration-none">${endpoint.path}</a> (FHIR v${endpoint.fhirVersion}${endpoint.context ? ', context: ' + endpoint.context : ''})</li>`;
426
363
  }
427
364
  content += '</ul>';
@@ -433,8 +370,51 @@ async function buildRootPageContent() {
433
370
 
434
371
  content += '<hr/>';
435
372
 
373
+ // Calculate uptime
374
+ const uptimeMs = Date.now() - stats.startTime;
375
+ const uptimeSeconds = Math.floor(uptimeMs / 1000);
376
+ const uptimeDays = Math.floor(uptimeSeconds / 86400);
377
+ const uptimeHours = Math.floor((uptimeSeconds % 86400) / 3600);
378
+ const uptimeMinutes = Math.floor((uptimeSeconds % 3600) / 60);
379
+ const uptimeSecs = uptimeSeconds % 60;
380
+ let uptimeStr = '';
381
+ if (uptimeDays > 0) uptimeStr += `${uptimeDays}d `;
382
+ if (uptimeHours > 0 || uptimeDays > 0) uptimeStr += `${uptimeHours}h `;
383
+ if (uptimeMinutes > 0 || uptimeHours > 0 || uptimeDays > 0) uptimeStr += `${uptimeMinutes}m `;
384
+ uptimeStr += `${uptimeSecs}s`;
436
385
 
437
- content += await buildDashboardContent();
386
+ // Memory usage
387
+ const memUsage = process.memoryUsage();
388
+ const heapUsedMB = (memUsage.heapUsed / 1024 / 1024).toFixed(2);
389
+ const heapAvailableMB = ((memUsage.heapTotal - memUsage.heapUsed) / 1024 / 1024).toFixed(2);
390
+ const rssMB = (memUsage.rss / 1024 / 1024).toFixed(2);
391
+ const freeMemMB = (os.freemem() / 1024 / 1024).toFixed(0);
392
+ const totalMemMB = (os.totalmem() / 1024 / 1024).toFixed(0);
393
+
394
+ content += '<table class="grid">';
395
+ content += '<tr>';
396
+ content += `<td><strong>Uptime:</strong> ${escape(uptimeStr)}</td>`;
397
+ content += `<td><strong>Request Count:</strong> ${stats.requestCount} (static: ${stats.staticRequestCount})</td>`;
398
+ content += `<td><strong>Free Memory:</strong> ${freeMemMB} MB of ${totalMemMB} MB</td>`;
399
+ content += '</tr>';
400
+ content += '<tr>';
401
+ content += `<td><strong>Heap Used:</strong> ${heapUsedMB} MB</td>`;
402
+ content += `<td><strong>Heap Available:</strong> ${heapAvailableMB} MB</td>`;
403
+ content += `<td><strong>Process Memory:</strong> ${rssMB} MB</td>`;
404
+ content += '</tr>';
405
+ content += getLogStats();
406
+ content += '</table>';
407
+
408
+ // ===== Metrics Graphs =====
409
+ const liquid = new Liquid({
410
+ root: path.join(__dirname, 'tx', 'html'),
411
+ extname: '.liquid'
412
+ });
413
+ content += await liquid.renderFile('home-metrics', {
414
+ historyJson: JSON.stringify(stats.history),
415
+ startTime: stats.startTime
416
+ });
417
+ content += stats.taskDetails();
438
418
 
439
419
  content += '</div>';
440
420
  return content;
@@ -546,8 +526,52 @@ app.get('/dashboard', async (req, res) => {
546
526
  }
547
527
 
548
528
  const startTime = Date.now();
549
- const dashContent = await buildDashboardContent();
550
- const content = '<div class="row mb-4"><div class="col-12">' + dashContent + '</div></div>';
529
+ // Calculate uptime
530
+ const uptimeMs = Date.now() - stats.startTime;
531
+ const uptimeSeconds = Math.floor(uptimeMs / 1000);
532
+ const uptimeDays = Math.floor(uptimeSeconds / 86400);
533
+ const uptimeHours = Math.floor((uptimeSeconds % 86400) / 3600);
534
+ const uptimeMinutes = Math.floor((uptimeSeconds % 3600) / 60);
535
+ const uptimeSecs = uptimeSeconds % 60;
536
+ let uptimeStr = '';
537
+ if (uptimeDays > 0) uptimeStr += `${uptimeDays}d `;
538
+ if (uptimeHours > 0 || uptimeDays > 0) uptimeStr += `${uptimeHours}h `;
539
+ if (uptimeMinutes > 0 || uptimeHours > 0 || uptimeDays > 0) uptimeStr += `${uptimeMinutes}m `;
540
+ uptimeStr += `${uptimeSecs}s`;
541
+
542
+ // Memory usage
543
+ const memUsage = process.memoryUsage();
544
+ const heapUsedPCT = (memUsage.heapUsed * 100) / memUsage.heapTotal;
545
+ const freeMemMB = (os.freemem() / 1024 / 1024).toFixed(0);
546
+ const totalMemMB = (os.totalmem() / 1024 / 1024).toFixed(0);
547
+ const usedMemPCT = 100 - ((freeMemMB * 100) / totalMemMB);
548
+ const fstats = fs.statfsSync(folders.logsDir());
549
+ const diskPCT = (fstats.bavail * 100) / fstats.blocks;
550
+
551
+ let content = '<style>table.grid{margin-bottom:10px;border:1px solid black;margin-right:auto}table.grid th,table.grid td{border:1px solid silver;padding:3px 7px 2px;font-size:12px;line-height:1.4em;font-family:verdana;vertical-align:top}table.grid th{font-weight:bold}table.grid td{font-weight:normal}</style>';
552
+ content += '<table class="grid">';
553
+ content += '<tr>';
554
+ content += `<td><strong>Uptime:</strong> ${escape(uptimeStr)}</td>`;
555
+ content += `<td><strong>Request Count:</strong> ${stats.requestCount} (static: ${stats.staticRequestCount})</td>`;
556
+ content += `<td style="background-color:${pctColor(usedMemPCT)}"><strong>Memory:</strong> ${usedMemPCT.toFixed(0)}%</td>`;
557
+ content += `<td style="background-color:${pctColor(heapUsedPCT)}"><strong>Heap:</strong> ${heapUsedPCT.toFixed(0)}%</td>`;
558
+ content += `<td style="background-color:${pctColor(diskPCT)}"><strong>Disk:</strong> ${diskPCT.toFixed(0)}%</td>`;
559
+ content += '</tr>';
560
+ content += '</table>';
561
+
562
+ // ===== Metrics Graphs =====
563
+ const liquid = new Liquid({
564
+ root: path.join(__dirname, 'tx', 'html'),
565
+ extname: '.liquid'
566
+ });
567
+ content += await liquid.renderFile('dash-metrics', {
568
+ historyJson: JSON.stringify(stats.history),
569
+ startTime: stats.startTime
570
+ });
571
+ content += stats.taskDetails();
572
+ content += `<p>Data: ${folders.dataDir()}</p>`;
573
+
574
+ content = '<div class="row mb-4"><div class="col-12">' + content + '</div></div>';
551
575
 
552
576
  const pageStats = {
553
577
  version: packageJson.version,
@@ -565,6 +589,12 @@ app.get('/dashboard', async (req, res) => {
565
589
  }
566
590
  });
567
591
 
592
+ function pctColor(pct) {
593
+ const r = Math.round(pct * 2.55);
594
+ const g = Math.round((100 - pct) * 2.55);
595
+ return `rgb(${r}, ${g}, 100)`; // the 100 keeps it pastel/light
596
+ }
597
+
568
598
  // Health check endpoint
569
599
  app.get('/health', async (req, res) => {
570
600
  const healthStatus = {
@@ -611,10 +641,10 @@ function getLogStats() {
611
641
  let diskInfo = '';
612
642
  try {
613
643
  // statfs available in Node 18.15+
614
- const stats = fs.statfsSync(logDir);
615
- const blockSize = stats.bsize;
616
- const freeSpace = stats.bavail * blockSize;
617
- const totalSpace = stats.blocks * blockSize;
644
+ const fstats = fs.statfsSync(logDir);
645
+ const blockSize = fstats.bsize;
646
+ const freeSpace = fstats.bavail * blockSize;
647
+ const totalSpace = fstats.blocks * blockSize;
618
648
  const freeGB = (freeSpace / 1024 / 1024 / 1024).toFixed(2);
619
649
  const totalGB = (totalSpace / 1024 / 1024 / 1024).toFixed(2);
620
650
  diskInfo = `<td><strong>Disk Space:</strong> ${freeGB} GB of ${totalGB} GB</td>`;
package/stats.js CHANGED
@@ -84,6 +84,7 @@ class ServerStats {
84
84
  this.taskMap.set(name, info);
85
85
  info.frequency = frequency;
86
86
  info.state = "Started";
87
+ info.status = "started"
87
88
  }
88
89
 
89
90
  task(name, state) {
@@ -91,6 +92,25 @@ class ServerStats {
91
92
  if (info) {
92
93
  info.date = Date.now();
93
94
  info.state = state;
95
+ info.status = 'working';
96
+ }
97
+ }
98
+
99
+ taskDone(name, state) {
100
+ let info = this.taskMap.get(name);
101
+ if (info) {
102
+ info.date = Date.now();
103
+ info.state = state;
104
+ info.status = 'resting';
105
+ }
106
+ }
107
+
108
+ taskError(name, state) {
109
+ let info = this.taskMap.get(name);
110
+ if (info) {
111
+ info.date = Date.now();
112
+ info.state = state;
113
+ info.status = 'error';
94
114
  }
95
115
  }
96
116
 
@@ -98,17 +118,19 @@ class ServerStats {
98
118
  if (this.taskMap.size == 0) {
99
119
  return "";
100
120
  }
101
- let html = '<table class="grid"><tr style="background-color: #EEEEEE"><th colspan="4">Background Tasks</th></tr>';
102
- html += "<tr><th>Task</th><th>Status</th><th>Frequency</th><th>Last Seen</th></tr>";
121
+ let html = '<table class="grid" >';
122
+ html += "<tr><th>Background Task</th><th>Status</th><th>Frequency</th><th>Last Seen</th></tr>";
103
123
  for (let m of this.taskMap.keys()) {
104
- html += "<tr><td>";
124
+ let mm = this.taskMap.get(m);
125
+ let color = this.getTaskColor(mm.status);
126
+ html += `<tr style="background-color: ${color}"><td>`;
105
127
  html += escape(m);
106
128
  html += "</td><td>";
107
- html += escape(this.taskMap.get(m).state);
129
+ html += escape(mm.state);
108
130
  html += "</td><td>";
109
- html += this.taskMap.get(m).frequency;
131
+ html += mm.frequency;
110
132
  html += "</td><td>";
111
- html += Utilities.formatDuration(this.taskMap.get(m).date, Date.now());
133
+ html += Utilities.formatDuration(mm.date, Date.now());
112
134
  html += "</td></tr>";
113
135
  }
114
136
  html += "</table>";
@@ -130,5 +152,14 @@ class ServerStats {
130
152
  return { idle, total };
131
153
  }
132
154
 
155
+ getTaskColor(status) {
156
+ switch (status) {
157
+ case "started": return "LightGrey";
158
+ case "working": return "LightGreen";
159
+ case "resting": return "White";
160
+ case "error": return "LightRed";
161
+ default: return "DarkBlue"; // should not happen
162
+ }
163
+ }
133
164
  }
134
165
  module.exports = ServerStats;
package/tx/cs/cs-api.js CHANGED
@@ -673,11 +673,13 @@ class CodeSystemProvider {
673
673
  /**
674
674
  * register the concept maps that are implicitly defined as part of the code system
675
675
  *
676
+ * @param {ConceptMap} map the map (this will have been returned from findImplicitConceptMap)
676
677
  * @param {Coding} coding the coding to translate
677
- * @param {String} target
678
- * @returns {CodeTranslation[]} the list of translations
678
+ * @param {String} target the target code system
679
+ * @param {boolean} reverse - if the translation is being run backwards
680
+ * @returns {CodeTranslation[]} the list of translations, each CodeTranslation has map, code, system, version, display, and relationship
679
681
  */
680
- async getTranslations(coding, target) { return null;}
682
+ async getTranslations(map, coding, target, reverse) { return null;}
681
683
 
682
684
  // ==== Parameter checking methods =========
683
685
  _ensureLanguages(param) {
@@ -853,7 +855,7 @@ class CodeSystemFactoryProvider {
853
855
  }
854
856
 
855
857
  /**
856
- * see comemnts for registerSupplements()
858
+ * see comments for registerSupplements()
857
859
  *
858
860
  * @param {CodeSystem} supplement - the supplement to flesh out
859
861
  * @returns void
@@ -865,6 +867,8 @@ class CodeSystemFactoryProvider {
865
867
  /**
866
868
  * build and return a known concept map from the URL, if there is one.
867
869
  *
870
+ * the conceptmap is never visible to a user; if it has an implicitSource, then
871
+ * provider.getTranslations will be called when it's actually used
868
872
  * @param url
869
873
  * @param version
870
874
  * @returns {ConceptMap}
package/tx/cs/cs-loinc.js CHANGED
@@ -5,6 +5,7 @@ const { Language, Languages} = require('../../library/languages');
5
5
  const { CodeSystemFactoryProvider} = require('./cs-api');
6
6
  const { validateOptionalParameter, validateArrayParameter} = require("../../library/utilities");
7
7
  const {BaseCSServices} = require("./cs-base");
8
+ const {sqlEscapeString} = require("../../xig/xig");
8
9
 
9
10
  // Context kinds matching Pascal enum
10
11
  const LoincProviderContextKind = {
@@ -657,13 +658,17 @@ class LoincServices extends BaseCSServices {
657
658
  }
658
659
 
659
660
  async filter(filterContext, prop, op, value) {
660
-
661
-
662
661
  const filter = new LoincFilterHolder();
663
662
  await this.#executeFilterQuery(prop, op, value, filter);
664
663
  filterContext.filters.push(filter);
665
664
  }
666
665
 
666
+ async searchFilter(filterContext, filterText, sort) {
667
+ const filter = new LoincFilterHolder();
668
+ await this.#executeFilterQuery('$text', (sort ? '>' : '<'), filterText.filter, filter);
669
+ filterContext.filters.push(filter);
670
+ }
671
+
667
672
  async #executeFilterQuery(prop, op, value, filter) {
668
673
  let sql = '';
669
674
  let lsql = '';
@@ -913,6 +918,13 @@ class LoincServices extends BaseCSServices {
913
918
  WHERE CodeKey IN (SELECT CodeKey FROM Properties WHERE PropertyTypeKey = 9)
914
919
  AND CodeKey = `;
915
920
  }
921
+ } else if (prop === '$text' && (op === '>' || op === '<')) {
922
+ sql = `SELECT CodeKey as Key FROM Codes
923
+ WHERE Description like '%${sqlEscapeString(value)}%'
924
+ ORDER BY Description `+(op === '<' ? 'ASC' : 'DESC');
925
+ lsql = `SELECT COUNT(CodeKey) as Key FROM Codes
926
+ WHERE Codes.Description like '%${sqlEscapeString(value)}%'
927
+ AND TargetKey = `;
916
928
  }
917
929
 
918
930
  if (sql) {
package/tx/cs/cs-omop.js CHANGED
@@ -660,8 +660,10 @@ class OMOPServices extends BaseCSServices {
660
660
  }
661
661
 
662
662
  // Translation support
663
- async getTranslations(coding, target) {
664
-
663
+ async getTranslations(map, coding, target) {
664
+ if (map == null) {
665
+ return;
666
+ }
665
667
 
666
668
  const vocabId = getVocabId(target);
667
669
  if (vocabId === -1) {
@@ -680,7 +682,7 @@ class OMOPServices extends BaseCSServices {
680
682
  reject(err);
681
683
  } else {
682
684
  const translations = rows.map(row => ({
683
- uri: target,
685
+ system: target,
684
686
  code: row.concept_code,
685
687
  display: row.concept_name,
686
688
  relationship: 'equivalent',