fhirsmith 0.4.2 → 0.5.1

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 (92) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +1 -1
  3. package/library/cron-utilities.js +136 -0
  4. package/library/html-server.js +13 -29
  5. package/library/html.js +3 -8
  6. package/library/languages.js +160 -37
  7. package/library/package-manager.js +48 -1
  8. package/library/utilities.js +100 -19
  9. package/package.json +2 -2
  10. package/packages/package-crawler.js +6 -1
  11. package/packages/packages.js +38 -54
  12. package/publisher/publisher.js +19 -27
  13. package/registry/api.js +11 -10
  14. package/registry/crawler.js +31 -29
  15. package/registry/model.js +5 -26
  16. package/registry/registry.js +32 -41
  17. package/server.js +89 -12
  18. package/shl/shl.js +0 -18
  19. package/static/assets/js/statuspage.js +1 -9
  20. package/stats.js +39 -1
  21. package/token/token.js +14 -9
  22. package/translations/Messages.properties +2 -1
  23. package/tx/README.md +17 -6
  24. package/tx/cs/cs-api.js +19 -1
  25. package/tx/cs/cs-base.js +77 -0
  26. package/tx/cs/cs-country.js +46 -0
  27. package/tx/cs/cs-cpt.js +9 -5
  28. package/tx/cs/cs-cs.js +27 -13
  29. package/tx/cs/cs-lang.js +60 -22
  30. package/tx/cs/cs-loinc.js +69 -98
  31. package/tx/cs/cs-mimetypes.js +4 -0
  32. package/tx/cs/cs-ndc.js +6 -0
  33. package/tx/cs/cs-omop.js +16 -15
  34. package/tx/cs/cs-rxnorm.js +23 -1
  35. package/tx/cs/cs-snomed.js +283 -40
  36. package/tx/cs/cs-ucum.js +90 -70
  37. package/tx/importers/import-sct.module.js +371 -35
  38. package/tx/importers/readme.md +117 -7
  39. package/tx/library/bundle.js +5 -0
  40. package/tx/library/capabilitystatement.js +3 -142
  41. package/tx/library/codesystem.js +19 -173
  42. package/tx/library/conceptmap.js +4 -218
  43. package/tx/library/designations.js +14 -1
  44. package/tx/library/extensions.js +7 -0
  45. package/tx/library/namingsystem.js +3 -89
  46. package/tx/library/operation-outcome.js +8 -3
  47. package/tx/library/parameters.js +3 -2
  48. package/tx/library/renderer.js +10 -6
  49. package/tx/library/terminologycapabilities.js +3 -243
  50. package/tx/library/valueset.js +3 -235
  51. package/tx/library.js +100 -13
  52. package/tx/operation-context.js +23 -4
  53. package/tx/params.js +35 -38
  54. package/tx/provider.js +6 -5
  55. package/tx/sct/expressions.js +12 -3
  56. package/tx/tx-html.js +80 -89
  57. package/tx/tx.fhir.org.yml +6 -5
  58. package/tx/tx.js +163 -13
  59. package/tx/vs/vs-database.js +56 -39
  60. package/tx/vs/vs-package.js +21 -2
  61. package/tx/vs/vs-vsac.js +175 -39
  62. package/tx/workers/batch-validate.js +2 -0
  63. package/tx/workers/batch.js +2 -0
  64. package/tx/workers/expand.js +132 -112
  65. package/tx/workers/lookup.js +33 -14
  66. package/tx/workers/metadata.js +2 -2
  67. package/tx/workers/read.js +3 -2
  68. package/tx/workers/related.js +574 -0
  69. package/tx/workers/search.js +46 -9
  70. package/tx/workers/subsumes.js +13 -3
  71. package/tx/workers/translate.js +7 -3
  72. package/tx/workers/validate.js +258 -285
  73. package/tx/workers/worker.js +43 -39
  74. package/tx/xml/bundle-xml.js +237 -0
  75. package/tx/xml/xml-base.js +215 -64
  76. package/tx/xversion/xv-bundle.js +71 -0
  77. package/tx/xversion/xv-capabiliityStatement.js +137 -0
  78. package/tx/xversion/xv-codesystem.js +169 -0
  79. package/tx/xversion/xv-conceptmap.js +224 -0
  80. package/tx/xversion/xv-namingsystem.js +88 -0
  81. package/tx/xversion/xv-operationoutcome.js +27 -0
  82. package/tx/xversion/xv-parameters.js +87 -0
  83. package/tx/xversion/xv-resource.js +45 -0
  84. package/tx/xversion/xv-terminologyCapabilities.js +214 -0
  85. package/tx/xversion/xv-valueset.js +234 -0
  86. package/utilities/dev-proxy-server.js +126 -0
  87. package/utilities/explode-results.js +58 -0
  88. package/utilities/split-by-system.js +198 -0
  89. package/utilities/vsac-cs-fetcher.js +0 -0
  90. package/{windows-install.js → utilities/windows-install.js} +2 -0
  91. package/vcl/vcl.js +0 -18
  92. package/xig/xig.js +241 -230
@@ -14,7 +14,7 @@ const MASTER_URL = 'https://fhir.github.io/ig-registry/tx-servers.json';
14
14
  class RegistryCrawler {
15
15
  log;
16
16
 
17
- constructor(config = {}) {
17
+ constructor(config = {}, stats) {
18
18
  this.config = {
19
19
  timeout: config.timeout || 30000, // 30 seconds default
20
20
  masterUrl: config.masterUrl || MASTER_URL,
@@ -22,7 +22,8 @@ class RegistryCrawler {
22
22
  crawlInterval: config.crawlInterval || 5 * 60 * 1000, // 5 minutes default
23
23
  apiKeys: config.apiKeys || {} // Map of server URL or code to API key
24
24
  };
25
-
25
+ this.stats = stats;
26
+
26
27
  this.currentData = new ServerRegistries();
27
28
  this.crawlTimer = null;
28
29
  this.isCrawling = false;
@@ -35,32 +36,32 @@ class RegistryCrawler {
35
36
  this.log = logv;
36
37
  }
37
38
 
38
- /**
39
- * Start the crawler with periodic updates
40
- */
41
- start() {
42
- if (this.crawlTimer) {
43
- return; // Already running
44
- }
45
-
46
- // Initial crawl
47
- this.crawl();
48
-
49
- // Set up periodic crawling
50
- this.crawlTimer = setInterval(() => {
51
- this.crawl();
52
- }, this.config.crawlInterval);
53
- }
54
-
55
- /**
56
- * Stop the crawler
57
- */
58
- stop() {
59
- if (this.crawlTimer) {
60
- clearInterval(this.crawlTimer);
61
- this.crawlTimer = null;
62
- }
63
- }
39
+ // /**
40
+ // * Start the crawler with periodic updates
41
+ // */
42
+ // start() {
43
+ // if (this.crawlTimer) {
44
+ // return; // Already running
45
+ // }
46
+ //
47
+ // // Initial crawl
48
+ // this.crawl();
49
+ //
50
+ // // Set up periodic crawling
51
+ // this.crawlTimer = setInterval(() => {
52
+ // this.crawl();
53
+ // }, this.config.crawlInterval);
54
+ // }
55
+ //
56
+ // /**
57
+ // * Stop the crawler
58
+ // */
59
+ // stop() {
60
+ // if (this.crawlTimer) {
61
+ // clearInterval(this.crawlTimer);
62
+ // this.crawlTimer = null;
63
+ // }
64
+ // }
64
65
 
65
66
  /**
66
67
  * Main entry point - crawl the registry starting from the master URL
@@ -133,7 +134,8 @@ class RegistryCrawler {
133
134
  registry.name = registryConfig.name;
134
135
  registry.authority = registryConfig.authority || '';
135
136
  registry.address = registryConfig.url;
136
-
137
+ this.stats.task('TxRegistry', 'Checking: '+registry.address);
138
+
137
139
  if (!registry.name) {
138
140
  this.addLogEntry('error', 'No name provided for registry', registryConfig.url);
139
141
  return registry;
package/registry/model.js CHANGED
@@ -1,5 +1,6 @@
1
1
  // registry-model.js
2
2
  // Data model for terminology server registry
3
+ const escape = require('escape-html');
3
4
 
4
5
  class ServerVersionInformation {
5
6
  constructor() {
@@ -36,28 +37,17 @@ class ServerVersionInformation {
36
37
  getCsListHtml() {
37
38
  if (this.codeSystems.length === 0) return '<ul></ul>';
38
39
  return '<ul>' + this.codeSystems.map(cs =>
39
- `<li>${this._escapeHtml(cs)}</li>`
40
+ `<li>${escape(cs)}</li>`
40
41
  ).join('') + '</ul>';
41
42
  }
42
43
 
43
44
  getVsListHtml() {
44
45
  if (this.valueSets.length === 0) return '<ul></ul>';
45
46
  return '<ul>' + this.valueSets.map(vs =>
46
- `<li>${this._escapeHtml(vs)}</li>`
47
+ `<li>${escape(vs)}</li>`
47
48
  ).join('') + '</ul>';
48
49
  }
49
50
 
50
- _escapeHtml(text) {
51
- const map = {
52
- '&': '&amp;',
53
- '<': '&lt;',
54
- '>': '&gt;',
55
- '"': '&quot;',
56
- "'": '&#039;'
57
- };
58
- return text.replace(/[&<>"']/g, m => map[m]);
59
- }
60
-
61
51
  toJSON() {
62
52
  return {
63
53
  version: this.version,
@@ -151,7 +141,7 @@ class ServerInformation {
151
141
  if (result) result += '. ';
152
142
  result += 'Authoritative for the following CodeSystems: <ul>';
153
143
  this.authCSList.forEach(cs => {
154
- const escaped = this._escapeHtml(cs).replace('*', '<b>*</b>');
144
+ const escaped = escape(cs).replace(/\*/g, '<b>*</b>');
155
145
  result += `<li>${escaped}</li>`;
156
146
  });
157
147
  result += '</ul>';
@@ -161,7 +151,7 @@ class ServerInformation {
161
151
  if (result) result += '. ';
162
152
  result += 'Authoritative for the following ValueSets: <ul>';
163
153
  this.authVSList.forEach(vs => {
164
- const escaped = this._escapeHtml(vs).replace('*', '<b>*</b>');
154
+ const escaped = escape(vs).replace(/\*/g, '<b>*</b>');
165
155
  result += `<li>${escaped}</li>`;
166
156
  });
167
157
  result += '</ul>';
@@ -170,17 +160,6 @@ class ServerInformation {
170
160
  return result;
171
161
  }
172
162
 
173
- _escapeHtml(text) {
174
- const map = {
175
- '&': '&amp;',
176
- '<': '&lt;',
177
- '>': '&gt;',
178
- '"': '&quot;',
179
- "'": '&#039;'
180
- };
181
- return text.replace(/[&<>"']/g, m => map[m]);
182
- }
183
-
184
163
  toJSON() {
185
164
  return {
186
165
  code: this.code,
@@ -8,6 +8,7 @@ const htmlServer = require('../library/html-server');
8
8
  const Logger = require('../library/logger');
9
9
  const regLog = Logger.getInstance().child({ module: 'registry' });
10
10
  const folders = require('../library/folder-setup');
11
+ const escape = require('escape-html');
11
12
 
12
13
  class RegistryModule {
13
14
  constructor(stats) {
@@ -43,7 +44,7 @@ class RegistryModule {
43
44
  apiKeys: config.apiKeys || {}
44
45
  };
45
46
 
46
- this.crawler = new RegistryCrawler(crawlerConfig);
47
+ this.crawler = new RegistryCrawler(crawlerConfig, this.stats);
47
48
  this.crawler.useLog(regLog);
48
49
 
49
50
  // Initialize API with crawler
@@ -119,6 +120,7 @@ class RegistryModule {
119
120
  }, 5000);
120
121
 
121
122
  // Set up periodic crawling
123
+ this.stats.addTask("TxRegistry", `${intervalMinutes} min`);
122
124
  this.crawlInterval = setInterval(() => {
123
125
  this.performCrawl();
124
126
  }, intervalMs);
@@ -134,6 +136,7 @@ class RegistryModule {
134
136
  this.logger.info('Crawl already in progress, skipping...');
135
137
  return;
136
138
  }
139
+ this.stats.task('TxRegistry', 'Crawling');
137
140
 
138
141
  this.crawlInProgress = true;
139
142
  this.logger.info('Starting registry crawl...');
@@ -160,9 +163,10 @@ class RegistryModule {
160
163
  `Found ${newData.registries.length} registries, ` +
161
164
  `${metadata.errors.length} errors, ` +
162
165
  `downloaded ${this.crawler.formatBytes(metadata.totalBytes)}`);
163
-
166
+ this.stats.task('TxRegistry', 'Crawling Finished');
164
167
  } catch (error) {
165
168
  this.logger.error('Crawl failed:', error);
169
+ this.stats.task('TxRegistry', 'Crawling Error: '+error.message);
166
170
  } finally {
167
171
  this.crawlInProgress = false;
168
172
  }
@@ -487,15 +491,15 @@ class RegistryModule {
487
491
 
488
492
  for (const server of serverVersions) {
489
493
  html += '<tr>';
490
- html += `<td><a href="${server.serverUrl}" target="_blank">${this._escapeHtml(server.serverUrl)}</a></td>`;
491
- html += `<td>${this._escapeHtml(server.software.replace("Reference Server", "HealthIntersections"))}</td>`;
492
- html += `<td>${this._escapeHtml(server.authority.replace("Published by", ""))}</td>`;
493
- html += `<td>${this._escapeHtml(server.version)}</td>`;
494
- html += `<td>${this._escapeHtml(server.security || '')}</td>`;
494
+ html += `<td><a href="${server.serverUrl}" target="_blank">${escape(server.serverUrl)}</a></td>`;
495
+ html += `<td>${escape(server.software.replace("Reference Server", "HealthIntersections"))}</td>`;
496
+ html += `<td>${escape(server.authority.replace("Published by", ""))}</td>`;
497
+ html += `<td>${escape(server.version)}</td>`;
498
+ html += `<td>${escape(server.security || '')}</td>`;
495
499
  html += '<td>';
496
500
  if (server.usage && server.usage.length > 0) {
497
501
  const badges = server.usage.map(tag =>
498
- (tag == 'public' ? '' : `<span class="badge badge-info mr-1">${this._escapeHtml(tag)}</span>`)
502
+ (tag == 'public' ? '' : `<span class="badge badge-info mr-1">${escape(tag)}</span>`)
499
503
  );
500
504
  html += badges.join(' ');
501
505
  }
@@ -534,19 +538,6 @@ class RegistryModule {
534
538
  return html;
535
539
  }
536
540
 
537
- /**
538
- * Helper function to escape HTML special characters
539
- */
540
- _escapeHtml(text) {
541
- if (!text) return '';
542
- return text
543
- .replace(/&/g, '&amp;')
544
- .replace(/</g, '&lt;')
545
- .replace(/>/g, '&gt;')
546
- .replace(/"/g, '&quot;')
547
- .replace(/'/g, '&#039;');
548
- }
549
-
550
541
  /**
551
542
  * Gather information about authoritative code systems
552
543
  * @returns {Array} Array of objects with code system information
@@ -821,8 +812,8 @@ class RegistryModule {
821
812
  // First row for this code system
822
813
  const rowspan = cs.servers.length;
823
814
  html += '<tr>';
824
- html += `<td rowspan="${rowspan}">${this._highlightWildcard(this._escapeHtml(formattedMask))}</td>`;
825
- html += `<td><a href="${this._escapeHtml(cs.servers[0].url)}" target="_blank">${this._escapeHtml(cs.servers[0].url)}</a></td>`;
815
+ html += `<td rowspan="${rowspan}">${this._highlightWildcard(escape(formattedMask))}</td>`;
816
+ html += `<td><a href="${escape(cs.servers[0].url)}" target="_blank">${escape(cs.servers[0].url)}</a></td>`;
826
817
 
827
818
  // Format versions as R3/R4/R5
828
819
  const formattedVersions = cs.servers[0].versions.map(v => this._formatFhirVersion(v));
@@ -832,7 +823,7 @@ class RegistryModule {
832
823
  // Additional rows for this code system (if any)
833
824
  for (let i = 1; i < cs.servers.length; i++) {
834
825
  html += '<tr>';
835
- html += `<td><a href="${this._escapeHtml(cs.servers[i].url)}" target="_blank">${this._escapeHtml(cs.servers[i].url)}</a></td>`;
826
+ html += `<td><a href="${escape(cs.servers[i].url)}" target="_blank">${escape(cs.servers[i].url)}</a></td>`;
836
827
 
837
828
  // Format versions as R3/R4/R5
838
829
  const formattedVersions = cs.servers[i].versions.map(v => this._formatFhirVersion(v));
@@ -877,8 +868,8 @@ class RegistryModule {
877
868
  // First row for this value set
878
869
  const rowspan = vs.servers.length;
879
870
  html += '<tr>';
880
- html += `<td rowspan="${rowspan}">${this._highlightWildcard(this._escapeHtml(vs.mask))}</td>`;
881
- html += `<td><a href="${this._escapeHtml(vs.servers[0].url)}" target="_blank">${this._escapeHtml(vs.servers[0].url)}</a></td>`;
871
+ html += `<td rowspan="${rowspan}">${this._highlightWildcard(escape(vs.mask))}</td>`;
872
+ html += `<td><a href="${escape(vs.servers[0].url)}" target="_blank">${escape(vs.servers[0].url)}</a></td>`;
882
873
 
883
874
  // Format versions as R3/R4/R5
884
875
  const formattedVersions = vs.servers[0].versions.map(v => this._formatFhirVersion(v));
@@ -888,7 +879,7 @@ class RegistryModule {
888
879
  // Additional rows for this value set (if any)
889
880
  for (let i = 1; i < vs.servers.length; i++) {
890
881
  html += '<tr>';
891
- html += `<td><a href="${this._escapeHtml(vs.servers[i].url)}" target="_blank">${this._escapeHtml(vs.servers[i].url)}</a></td>`;
882
+ html += `<td><a href="${escape(vs.servers[i].url)}" target="_blank">${escape(vs.servers[i].url)}</a></td>`;
892
883
 
893
884
  // Format versions as R3/R4/R5
894
885
  const formattedVersions = vs.servers[i].versions.map(v => this._formatFhirVersion(v));
@@ -1103,11 +1094,11 @@ class RegistryModule {
1103
1094
  html += '<h2 class="card-title">Query Information</h2>';
1104
1095
  html += '</div>';
1105
1096
  html += '<div class="card-body">';
1106
- html += `<p><strong>FHIR Version:</strong> ${this._escapeHtml(fhirVersion)}</p>`;
1107
- html += `<p><strong>Resource URL:</strong> ${this._escapeHtml(resourceUrl)}</p>`;
1108
- html += `<p><strong>Registry URL:</strong> <a href="${result['registry-url']}" target="_blank">${this._escapeHtml(result['registry-url'])}</a></p>`;
1097
+ html += `<p><strong>FHIR Version:</strong> ${escape(fhirVersion)}</p>`;
1098
+ html += `<p><strong>Resource URL:</strong> ${escape(resourceUrl)}</p>`;
1099
+ html += `<p><strong>Registry URL:</strong> <a href="${result['registry-url']}" target="_blank">${escape(result['registry-url'])}</a></p>`;
1109
1100
  if (usage) {
1110
- html += `<p><strong>Usage:</strong> ${this._escapeHtml(usage)}</p>`;
1101
+ html += `<p><strong>Usage:</strong> ${escape(usage)}</p>`;
1111
1102
  }
1112
1103
  html += '</div>';
1113
1104
  html += '</div>';
@@ -1133,10 +1124,10 @@ class RegistryModule {
1133
1124
 
1134
1125
  result.authoritative.forEach(server => {
1135
1126
  html += '<tr>';
1136
- html += `<td>${this._escapeHtml(server['server-name'])}</td>`;
1137
- html += `<td><a href="${server.url}" target="_blank">${this._escapeHtml(server.url)}</a></td>`;
1127
+ html += `<td>${escape(server['server-name'])}</td>`;
1128
+ html += `<td><a href="${server.url}" target="_blank">${escape(server.url)}</a></td>`;
1138
1129
  html += `<td>${this.renderSecurityTags(server)}</td>`;
1139
- html += `<td>${server.access_info ? this._escapeHtml(server.access_info) : ''}</td>`;
1130
+ html += `<td>${server.access_info ? escape(server.access_info) : ''}</td>`;
1140
1131
  html += '</tr>';
1141
1132
  });
1142
1133
 
@@ -1170,10 +1161,10 @@ class RegistryModule {
1170
1161
 
1171
1162
  result.candidates.forEach(server => {
1172
1163
  html += '<tr>';
1173
- html += `<td>${this._escapeHtml(server['server-name'])}</td>`;
1174
- html += `<td><a href="${server.url}" target="_blank">${this._escapeHtml(server.url)}</a></td>`;
1164
+ html += `<td>${escape(server['server-name'])}</td>`;
1165
+ html += `<td><a href="${server.url}" target="_blank">${escape(server.url)}</a></td>`;
1175
1166
  html += `<td>${this.renderSecurityTags(server)}</td>`;
1176
- html += `<td>${server.access_info ? this._escapeHtml(server.access_info) : ''}</td>`;
1167
+ html += `<td>${server.access_info ? escape(server.access_info) : ''}</td>`;
1177
1168
  html += '</tr>';
1178
1169
  });
1179
1170
 
@@ -1230,7 +1221,7 @@ class RegistryModule {
1230
1221
  html += '<p>';
1231
1222
  html += '<label for="fhirVersion" class="form-label fw-bold">FHIR Version <span class="text-danger">*</span></label>';
1232
1223
  html += `<input type="text" class="form-control" id="fhirVersion" name="fhirVersion" size="8"
1233
- value="${this._escapeHtml(fhirVersion)}" required>`;
1224
+ value="${escape(fhirVersion)}" required>`;
1234
1225
  html += '</p>';
1235
1226
  html += '<p class="text-muted small">Examples: R4, 4.0.1, 5.0.0, etc.</p>';
1236
1227
 
@@ -1238,7 +1229,7 @@ class RegistryModule {
1238
1229
  html += '<p>';
1239
1230
  html += '<label for="url" class="form-label fw-bold">Code System URL</label>';
1240
1231
  html += `<input type="url" class="form-control" id="url" name="url"
1241
- value="${this._escapeHtml(url)}">`;
1232
+ value="${escape(url)}">`;
1242
1233
  html += '</p>';
1243
1234
  html += '<p class="text-muted small">Example: http://loinc.org</p>';
1244
1235
 
@@ -1246,7 +1237,7 @@ class RegistryModule {
1246
1237
  html += '<p>';
1247
1238
  html += '<label for="valueSet" class="form-label fw-bold">Value Set URL</label>';
1248
1239
  html += `<input type="url" class="form-control" id="valueSet" name="valueSet"
1249
- value="${this._escapeHtml(valueSet)}">`;
1240
+ value="${escape(valueSet)}">`;
1250
1241
  html += '</p>';
1251
1242
  html += '<p class="text-muted small">Example: http://hl7.org/fhir/ValueSet/observation-codes</p>';
1252
1243
 
@@ -1382,7 +1373,7 @@ class RegistryModule {
1382
1373
  }
1383
1374
 
1384
1375
  // Format: [time] [LEVEL] message
1385
- html += `<span style="color: #666;">[${timeDisplay}]</span> <span style="${levelStyle}">[${log.level.toUpperCase()}]</span> ${this._escapeHtml(log.message)}\n`;
1376
+ html += `<span style="color: #666;">[${timeDisplay}]</span> <span style="${levelStyle}">[${log.level.toUpperCase()}]</span> ${escape(log.message)}\n`;
1386
1377
  });
1387
1378
  }
1388
1379
 
package/server.js CHANGED
@@ -10,7 +10,10 @@ const express = require('express');
10
10
  // const cors = require('cors');
11
11
  const path = require('path');
12
12
  const fs = require('fs');
13
+ const os = require('os');
13
14
  const folders = require('./library/folder-setup'); // <-- ADD: load early
15
+ const { statSync, readdirSync } = require('fs');
16
+ const escape = require('escape-html');
14
17
 
15
18
  // Load configuration BEFORE logger
16
19
  let config;
@@ -25,6 +28,17 @@ try {
25
28
 
26
29
  const Logger = require('./library/logger');
27
30
  const serverLog = Logger.getInstance().child({ module: 'server' });
31
+ const packageJson = require('./package.json');
32
+
33
+ // Startup banner
34
+ const totalMemGB = (os.totalmem() / 1024 / 1024 / 1024).toFixed(1);
35
+ const freeMemGB = (os.freemem() / 1024 / 1024 / 1024).toFixed(1);
36
+ serverLog.info(`========================================`);
37
+ serverLog.info(`FHIRsmith v${packageJson.version} starting (PID ${process.pid})`);
38
+ serverLog.info(`Node.js ${process.version} on ${os.type()} ${os.release()} (${os.arch()})`);
39
+ serverLog.info(`Memory: ${freeMemGB} GB free / ${totalMemGB} GB total`);
40
+ serverLog.info(`Data directory: ${folders.dataDir()}`);
41
+ serverLog.info(`========================================`);
28
42
 
29
43
  const activeModules = config.modules ? Object.keys(config.modules)
30
44
  .filter(mod => config.modules[mod].enabled)
@@ -41,12 +55,11 @@ const PublisherModule = require('./publisher/publisher.js');
41
55
  const TokenModule = require('./token/token.js');
42
56
  const NpmProjectorModule = require('./npmprojector/npmprojector.js');
43
57
  const TXModule = require('./tx/tx.js');
44
- const packageJson = require('./package.json');
45
58
 
46
59
  const htmlServer = require('./library/html-server');
47
60
  const ServerStats = require("./stats");
48
61
  const {Liquid} = require("liquidjs");
49
- const {escapeHtml} = require("./library/utilities");
62
+
50
63
  htmlServer.useLog(serverLog);
51
64
 
52
65
  const app = express();
@@ -79,6 +92,7 @@ async function initializeModules() {
79
92
  // Initialize SHL module
80
93
  if (config.modules?.shl?.enabled) {
81
94
  try {
95
+ serverLog.info('Initializing module: shl...');
82
96
  modules.shl = new SHLModule(stats);
83
97
  await modules.shl.initialize(config.modules.shl);
84
98
  app.use('/shl', modules.shl.router);
@@ -91,6 +105,7 @@ async function initializeModules() {
91
105
  // Initialize VCL module
92
106
  if (config.modules?.vcl?.enabled) {
93
107
  try {
108
+ serverLog.info('Initializing module: vcl...');
94
109
  modules.vcl = new VCLModule(stats);
95
110
  await modules.vcl.initialize(config.modules.vcl);
96
111
  app.use('/VCL', modules.vcl.router);
@@ -99,11 +114,12 @@ async function initializeModules() {
99
114
  throw error;
100
115
  }
101
116
  }
102
-
117
+
103
118
  // Initialize XIG module
104
119
  if (config.modules?.xig?.enabled) {
105
120
  try {
106
- await xigModule.initializeXigModule(stats);
121
+ serverLog.info('Initializing module: xig...');
122
+ await xigModule.initializeXigModule(stats, config.modules.xig);
107
123
  app.use('/xig', xigModule.router);
108
124
  modules.xig = xigModule;
109
125
  } catch (error) {
@@ -115,6 +131,7 @@ async function initializeModules() {
115
131
  // Initialize Packages module
116
132
  if (config.modules?.packages?.enabled) {
117
133
  try {
134
+ serverLog.info('Initializing module: packages...');
118
135
  modules.packages = new PackagesModule(stats);
119
136
  await modules.packages.initialize(config.modules.packages);
120
137
  app.use('/packages', modules.packages.router);
@@ -128,6 +145,7 @@ async function initializeModules() {
128
145
  // Initialize Registry module
129
146
  if (config.modules?.registry?.enabled) {
130
147
  try {
148
+ serverLog.info('Initializing module: registry...');
131
149
  modules.registry = new RegistryModule(stats);
132
150
  await modules.registry.initialize(config.modules.registry);
133
151
  app.use('/tx-reg', modules.registry.router);
@@ -140,6 +158,7 @@ async function initializeModules() {
140
158
  // Initialize Publisher module
141
159
  if (config.modules?.publisher?.enabled) {
142
160
  try {
161
+ serverLog.info('Initializing module: publisher...');
143
162
  modules.publisher = new PublisherModule(stats);
144
163
  await modules.publisher.initialize(config.modules.publisher);
145
164
  app.use('/publisher', modules.publisher.router);
@@ -152,6 +171,7 @@ async function initializeModules() {
152
171
  // Initialize Token module
153
172
  if (config.modules?.token?.enabled) {
154
173
  try {
174
+ serverLog.info('Initializing module: token...');
155
175
  modules.token = new TokenModule(stats);
156
176
  await modules.token.initialize(config.modules.token);
157
177
  app.use('/token', modules.token.router);
@@ -164,6 +184,7 @@ async function initializeModules() {
164
184
  // Initialize NpmProjector module
165
185
  if (config.modules?.npmprojector?.enabled) {
166
186
  try {
187
+ serverLog.info('Initializing module: npmprojector...');
167
188
  modules.npmprojector = new NpmProjectorModule(stats);
168
189
  await modules.npmprojector.initialize(config.modules.npmprojector);
169
190
  const basePath = NpmProjectorModule.getBasePath(config.modules.npmprojector);
@@ -179,10 +200,10 @@ async function initializeModules() {
179
200
  // because it supports multiple endpoints at different paths
180
201
  if (config.modules?.tx?.enabled) {
181
202
  try {
203
+ serverLog.info('Initializing module: tx...');
182
204
  modules.tx = new TXModule(stats);
183
205
  await modules.tx.initialize(config.modules.tx, app);
184
206
  } catch (error) {
185
- console.log(error);
186
207
  serverLog.error('Failed to initialize TX module:', error);
187
208
  throw error;
188
209
  }
@@ -333,7 +354,7 @@ async function buildRootPageContent() {
333
354
  content += '<table class="grid">';
334
355
  content += '<tr>';
335
356
  content += `<td><strong>Module Count:</strong> ${mc}</td>`;
336
- content += `<td><strong>Uptime:</strong> ${escapeHtml(uptimeStr)}</td>`;
357
+ content += `<td><strong>Uptime:</strong> ${escape(uptimeStr)}</td>`;
337
358
  content += `<td><strong>Request Count:</strong> ${stats.requestCount}</td>`;
338
359
  content += '</tr>';
339
360
  content += '<tr>';
@@ -341,7 +362,7 @@ async function buildRootPageContent() {
341
362
  content += `<td><strong>Heap Total:</strong> ${heapTotalMB} MB</td>`;
342
363
  content += `<td><strong>Process Memory:</strong> ${rssMB} MB</td>`;
343
364
  content += '</tr>';
344
-
365
+ content += getLogStats();
345
366
  content += '</table>';
346
367
 
347
368
 
@@ -355,6 +376,7 @@ async function buildRootPageContent() {
355
376
  historyJson: JSON.stringify(stats.history),
356
377
  startTime: stats.startTime
357
378
  });
379
+ content += stats.taskDetails();
358
380
 
359
381
  content += '</div>';
360
382
  return content;
@@ -363,6 +385,13 @@ async function buildRootPageContent() {
363
385
  // eslint-disable-next-line no-unused-vars
364
386
  process.on('unhandledRejection', (reason, promise) => {
365
387
  console.error('Unhandled Rejection:', reason);
388
+ serverLog.error('Unhandled Rejection:', reason);
389
+ });
390
+
391
+ process.on('uncaughtException', (error) => {
392
+ console.error('FATAL - Uncaught Exception:', error);
393
+ serverLog.error('FATAL - Uncaught Exception:', error);
394
+ process.exitCode = 1;
366
395
  });
367
396
 
368
397
  app.get('/', async (req, res) => {
@@ -380,7 +409,7 @@ app.get('/', async (req, res) => {
380
409
  }
381
410
 
382
411
  const content = await buildRootPageContent();
383
-
412
+
384
413
  // Build basic stats for root page
385
414
  const stats = {
386
415
  version: packageJson.version,
@@ -388,7 +417,7 @@ app.get('/', async (req, res) => {
388
417
  processingTime: Date.now() - startTime
389
418
  };
390
419
 
391
- const html = htmlServer.renderPage('root', config.hostName || 'FHIRsmith Server', content, stats);
420
+ const html = htmlServer.renderPage('root', escape(config.hostName) || 'FHIRsmith Server', content, stats);
392
421
  res.setHeader('Content-Type', 'text/html');
393
422
  res.send(html);
394
423
  } catch (error) {
@@ -429,7 +458,7 @@ app.get('/', async (req, res) => {
429
458
  Object.keys(enabledModules)
430
459
  .filter(m => m !== 'tx')
431
460
  .map(m => [
432
- m,
461
+ m,
433
462
  m === 'vcl' ? '/VCL' : `/${m}`
434
463
  ])
435
464
  ),
@@ -473,6 +502,52 @@ app.get('/health', async (req, res) => {
473
502
  res.json(healthStatus);
474
503
  });
475
504
 
505
+ /**
506
+ * Get log directory statistics: file count, total size, and disk space info
507
+ * @returns {string} HTML table row(s) with log stats
508
+ */
509
+ function getLogStats() {
510
+ const logDir = folders.logsDir();
511
+
512
+ try {
513
+ const files = readdirSync(logDir).filter(f => f.endsWith('.log'));
514
+ let totalSize = 0;
515
+ for (const file of files) {
516
+ totalSize += statSync(path.join(logDir, file)).size;
517
+ }
518
+
519
+ const sizeMB = (totalSize / 1024 / 1024).toFixed(2);
520
+
521
+ let diskInfo = '';
522
+ try {
523
+ // statfs available in Node 18.15+
524
+ const stats = fs.statfsSync(logDir);
525
+ const blockSize = stats.bsize;
526
+ const freeSpace = stats.bavail * blockSize;
527
+ const totalSpace = stats.blocks * blockSize;
528
+ const freeGB = (freeSpace / 1024 / 1024 / 1024).toFixed(2);
529
+ const totalGB = (totalSpace / 1024 / 1024 / 1024).toFixed(2);
530
+ diskInfo = `<td><strong>Disk Space:</strong> ${freeGB} GB of ${totalGB} GB</td>`;
531
+ } catch {
532
+ diskInfo = '<td><strong>Disk Space:</strong> unavailable</td>';
533
+ }
534
+
535
+ // Log rotation limit from logger config
536
+ const loggerOpts = Logger.getInstance().options;
537
+ const maxFiles = loggerOpts.file.maxFiles;
538
+ const maxSize = loggerOpts.file.maxSize;
539
+ const limitInfo = `${maxFiles} files × ${maxSize} each`;
540
+
541
+ return '<tr>'
542
+ + `<td><strong>Existing Logs:</strong> ${files.length} (${sizeMB} MB)</td>`
543
+ + `<td><strong>Retention Policy:</strong> ${limitInfo}</td>`
544
+ + diskInfo
545
+ + '</tr>';
546
+ } catch (e) {
547
+ return `<tr><td colspan="3"><strong>Logs:</strong> unable to read (${e.message})</td></tr>`;
548
+ }
549
+ }
550
+
476
551
  // Initialize everything
477
552
  async function startServer() {
478
553
  try {
@@ -494,8 +569,10 @@ async function startServer() {
494
569
  modules.packages.startInitialCrawler();
495
570
  }
496
571
  } catch (error) {
497
- serverLog.error('Failed to start server:', error);
498
- process.exit(1);
572
+ console.error('FATAL - Failed to start server:', error);
573
+ serverLog.error('FATAL - Failed to start server:', error);
574
+ // Give the logger a moment to flush before exiting
575
+ setTimeout(() => process.exit(1), 500);
499
576
  }
500
577
  }
501
578
 
package/shl/shl.js CHANGED
@@ -222,24 +222,6 @@ class SHLModule {
222
222
  }
223
223
  }
224
224
 
225
- // Enhanced HTML escaping
226
- escapeHtml(str) {
227
- if (!str || typeof str !== 'string') return '';
228
-
229
- const escapeMap = {
230
- '&': '&amp;',
231
- '<': '&lt;',
232
- '>': '&gt;',
233
- '"': '&quot;',
234
- "'": '&#x27;',
235
- '/': '&#x2F;',
236
- '`': '&#x60;',
237
- '=': '&#x3D;'
238
- };
239
-
240
- return str.replace(/[&<>"'`=/]/g, (match) => escapeMap[match]);
241
- }
242
-
243
225
  // URL validation
244
226
  validateExternalUrl(url) {
245
227
  try {
@@ -1,13 +1,5 @@
1
1
  <!--- admin -->
2
-
3
- String.prototype.escapeHTML = function () {
4
- return(
5
- this.replace(/&/g,'&amp;').
6
- replace(/>/g,'&gt;').
7
- replace(/</g,'&lt;').
8
- replace(/"/g,'&quot;')
9
- );
10
- };
2
+ const escape = require('escape-html');
11
3
 
12
4
  function setCookie(c_name,value,exdays)
13
5
  {