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.
- package/CHANGELOG.md +48 -0
- package/README.md +5 -1
- package/library/languages.js +10 -0
- package/package.json +1 -1
- package/packages/package-crawler.js +2 -2
- package/publisher/publisher.js +1 -1
- package/registry/registry.js +2 -2
- package/root-bare-template.html +1 -2
- package/security.md +3 -0
- package/server.js +100 -70
- package/stats.js +37 -6
- package/tx/cs/cs-api.js +8 -4
- package/tx/cs/cs-loinc.js +14 -2
- package/tx/cs/cs-omop.js +5 -3
- package/tx/cs/cs-rxnorm.js +18 -16
- package/tx/cs/cs-snomed.js +279 -6
- package/tx/data/cpt-fragment.db +0 -0
- package/tx/data/cs-de.json +186 -0
- package/tx/data/cs-extensions.json +92 -0
- package/tx/data/cs-simple.json +130 -0
- package/tx/data/cs-supplement.json +78 -0
- package/tx/data/lang.dat +49180 -0
- package/tx/data/languages.csv +191 -0
- package/tx/data/loinc-subset.txt +75 -0
- package/tx/data/omop-fragment.db +0 -0
- package/tx/data/readme.md +43 -0
- package/tx/data/regions.csv +273 -0
- package/tx/data/rxnorm-subset.txt +22 -0
- package/tx/data/snomed-subset.txt +47 -0
- package/tx/data/ucum-essence.xml +2059 -0
- package/tx/html/dash-metrics.liquid +147 -0
- package/tx/importers/import-rxnorm.module.js +4 -30
- package/tx/library/canonical-resource.js +8 -0
- package/tx/library/conceptmap.js +29 -1
- package/tx/library/designations.js +4 -8
- package/tx/library/extensions.js +4 -3
- package/tx/library/renderer.js +9 -9
- package/tx/ocl/cm-ocl.cjs +185 -65
- package/tx/ocl/cs-ocl.cjs +69 -50
- package/tx/ocl/jobs/background-queue.cjs +0 -8
- package/tx/ocl/mappers/concept-mapper.cjs +13 -3
- package/tx/ocl/shared/patches.cjs +1 -0
- package/tx/ocl/vs-ocl.cjs +137 -157
- package/tx/operation-context.js +3 -3
- package/tx/params.js +2 -2
- package/tx/provider.js +6 -3
- package/tx/sct/structures.js +6 -1
- package/tx/tx.fhir.org.yml +1 -1
- package/tx/vs/vs-database.js +107 -23
- package/tx/vs/vs-vsac.js +66 -19
- package/tx/workers/expand.js +10 -10
- package/tx/workers/related.js +2 -2
- package/tx/workers/search.js +2 -1
- package/tx/workers/translate.js +222 -33
- package/tx/workers/validate.js +13 -13
- package/tx/xversion/xv-parameters.js +54 -1
- 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)
|
package/library/languages.js
CHANGED
|
@@ -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
|
@@ -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.
|
|
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.
|
|
131
|
+
this.stats.taskError('Package Crawler', 'Error: '+error.message);
|
|
132
132
|
|
|
133
133
|
this.log.error('Web crawler failed: '+ error);
|
|
134
134
|
throw error;
|
package/publisher/publisher.js
CHANGED
|
@@ -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,
|
|
484
|
+
await this.updateTaskStatus(task.id, 'building', {
|
|
485
485
|
build_output_path: logFile,
|
|
486
486
|
local_folder: taskDir
|
|
487
487
|
});
|
package/registry/registry.js
CHANGED
|
@@ -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.
|
|
166
|
+
this.stats.taskDone('TxRegistry', 'Crawling Finished');
|
|
167
167
|
} catch (error) {
|
|
168
168
|
this.logger.error('Crawl failed:', error);
|
|
169
|
-
this.stats.
|
|
169
|
+
this.stats.taskError('TxRegistry', 'Crawling Error: '+error.message);
|
|
170
170
|
} finally {
|
|
171
171
|
this.crawlInProgress = false;
|
|
172
172
|
}
|
package/root-bare-template.html
CHANGED
package/security.md
CHANGED
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
|
-
|
|
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
|
-
|
|
550
|
-
const
|
|
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
|
|
615
|
-
const blockSize =
|
|
616
|
-
const freeSpace =
|
|
617
|
-
const totalSpace =
|
|
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"
|
|
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
|
-
|
|
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(
|
|
129
|
+
html += escape(mm.state);
|
|
108
130
|
html += "</td><td>";
|
|
109
|
-
html +=
|
|
131
|
+
html += mm.frequency;
|
|
110
132
|
html += "</td><td>";
|
|
111
|
-
html += Utilities.formatDuration(
|
|
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
|
-
* @
|
|
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
|
|
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
|
-
|
|
685
|
+
system: target,
|
|
684
686
|
code: row.concept_code,
|
|
685
687
|
display: row.concept_name,
|
|
686
688
|
relationship: 'equivalent',
|