fhirsmith 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +4 -2
  3. package/library/cron-utilities.js +136 -0
  4. package/library/folder-setup.js +6 -0
  5. package/library/html-server.js +13 -29
  6. package/library/html.js +3 -8
  7. package/library/languages.js +160 -37
  8. package/library/package-manager.js +48 -1
  9. package/library/utilities.js +100 -19
  10. package/package.json +2 -2
  11. package/packages/package-crawler.js +6 -1
  12. package/packages/packages.js +38 -54
  13. package/publisher/publisher.js +19 -27
  14. package/registry/api.js +11 -10
  15. package/registry/crawler.js +31 -29
  16. package/registry/model.js +5 -26
  17. package/registry/readme.md +1 -11
  18. package/registry/registry.js +32 -41
  19. package/server.js +53 -5
  20. package/shl/shl.js +0 -18
  21. package/static/assets/js/statuspage.js +1 -9
  22. package/stats.js +39 -1
  23. package/token/token.js +14 -9
  24. package/translations/Messages.properties +2 -1
  25. package/tx/README.md +17 -6
  26. package/tx/cs/cs-api.js +19 -1
  27. package/tx/cs/cs-base.js +77 -0
  28. package/tx/cs/cs-country.js +46 -0
  29. package/tx/cs/cs-cpt.js +9 -5
  30. package/tx/cs/cs-cs.js +27 -13
  31. package/tx/cs/cs-db.js +0 -13
  32. package/tx/cs/cs-lang.js +60 -22
  33. package/tx/cs/cs-loinc.js +69 -98
  34. package/tx/cs/cs-mimetypes.js +4 -0
  35. package/tx/cs/cs-ndc.js +6 -0
  36. package/tx/cs/cs-omop.js +16 -15
  37. package/tx/cs/cs-rxnorm.js +23 -1
  38. package/tx/cs/cs-snomed.js +283 -40
  39. package/tx/cs/cs-ucum.js +90 -70
  40. package/tx/importers/import-sct.module.js +371 -35
  41. package/tx/importers/readme.md +117 -7
  42. package/tx/library/bundle.js +5 -0
  43. package/tx/library/capabilitystatement.js +3 -142
  44. package/tx/library/codesystem.js +19 -173
  45. package/tx/library/conceptmap.js +4 -218
  46. package/tx/library/designations.js +14 -1
  47. package/tx/library/extensions.js +7 -0
  48. package/tx/library/namingsystem.js +3 -89
  49. package/tx/library/operation-outcome.js +8 -3
  50. package/tx/library/parameters.js +3 -2
  51. package/tx/library/renderer.js +10 -6
  52. package/tx/library/terminologycapabilities.js +3 -243
  53. package/tx/library/valueset.js +3 -235
  54. package/tx/library.js +100 -13
  55. package/tx/operation-context.js +23 -4
  56. package/tx/params.js +35 -38
  57. package/tx/provider.js +6 -5
  58. package/tx/sct/expressions.js +12 -3
  59. package/tx/tx-html.js +80 -89
  60. package/tx/tx.fhir.org.yml +6 -5
  61. package/tx/tx.js +163 -13
  62. package/tx/vs/vs-database.js +56 -39
  63. package/tx/vs/vs-package.js +21 -2
  64. package/tx/vs/vs-vsac.js +175 -39
  65. package/tx/workers/batch-validate.js +2 -0
  66. package/tx/workers/batch.js +2 -0
  67. package/tx/workers/expand.js +132 -112
  68. package/tx/workers/lookup.js +33 -14
  69. package/tx/workers/metadata.js +2 -2
  70. package/tx/workers/read.js +3 -2
  71. package/tx/workers/related.js +574 -0
  72. package/tx/workers/search.js +46 -9
  73. package/tx/workers/subsumes.js +13 -3
  74. package/tx/workers/translate.js +7 -3
  75. package/tx/workers/validate.js +258 -285
  76. package/tx/workers/worker.js +43 -39
  77. package/tx/xml/bundle-xml.js +237 -0
  78. package/tx/xml/xml-base.js +215 -64
  79. package/tx/xversion/xv-bundle.js +71 -0
  80. package/tx/xversion/xv-capabiliityStatement.js +137 -0
  81. package/tx/xversion/xv-codesystem.js +169 -0
  82. package/tx/xversion/xv-conceptmap.js +224 -0
  83. package/tx/xversion/xv-namingsystem.js +88 -0
  84. package/tx/xversion/xv-operationoutcome.js +27 -0
  85. package/tx/xversion/xv-parameters.js +87 -0
  86. package/tx/xversion/xv-resource.js +45 -0
  87. package/tx/xversion/xv-terminologyCapabilities.js +214 -0
  88. package/tx/xversion/xv-valueset.js +234 -0
  89. package/utilities/dev-proxy-server.js +126 -0
  90. package/utilities/explode-results.js +58 -0
  91. package/utilities/split-by-system.js +198 -0
  92. package/utilities/vsac-cs-fetcher.js +0 -0
  93. package/{windows-install.js → utilities/windows-install.js} +2 -0
  94. package/vcl/vcl.js +0 -18
  95. package/xig/xig.js +108 -99
  96. package/passwords.ini +0 -2
  97. package/registry/registry-data.json +0 -121015
  98. package/shl/private-key.pem +0 -5
  99. package/shl/public-key.pem +0 -18
  100. package/test-cache/vsac/vsac-valuesets.db +0 -0
  101. package/tx/dev.fhir.org.yml +0 -14
  102. package/tx/fixtures/test-cases-setup.json +0 -18
  103. package/tx/fixtures/test-cases.yml +0 -16
package/registry/api.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // Enhanced registry-api.js with resolver and HTML rendering functions
2
2
 
3
3
  const { ServerRegistryUtilities } = require('./model');
4
+ const escape = require('escape-html');
4
5
 
5
6
  class RegistryAPI {
6
7
  constructor(crawler) {
@@ -631,19 +632,19 @@ class RegistryAPI {
631
632
  html += '<tr>\n';
632
633
 
633
634
  if (!regCode) {
634
- html += `<td><a href="${path}&registry=${row['registry-code']}">${this._escapeHtml(row['registry-name'])}</a></td>\n`;
635
+ html += `<td><a href="${path}&registry=${row['registry-code']}">${escape(row['registry-name'])}</a></td>\n`;
635
636
  }
636
637
  if (!serverCode) {
637
- html += `<td><a href="${path}&server=${row['server-code']}">${this._escapeHtml(row['server-name'])}</a></td>\n`;
638
+ html += `<td><a href="${path}&server=${row['server-code']}">${escape(row['server-name'])}</a></td>\n`;
638
639
  }
639
640
  if (!versionCode) {
640
641
  html += `<td><a href="${path}&fhirVersion=${row.fhirVersion}">${row.fhirVersion}</a></td>\n`;
641
642
  }
642
643
 
643
- html += `<td><a href="${this._escapeHtml(row.url)}">${this._escapeHtml(row.url)}</a></td>\n`;
644
+ html += `<td><a href="${escape(row.url)}">${escape(row.url)}</a></td>\n`;
644
645
 
645
646
  if (row.error) {
646
- html += `<td><span style="color: maroon">Error: ${this._escapeHtml(row.error)}</span> Last OK ${this._formatDuration(row['last-success'])} ago</td>\n`;
647
+ html += `<td><span style="color: maroon">Error: ${escape(row.error)}</span> Last OK ${this._formatDuration(row['last-success'])} ago</td>\n`;
647
648
  } else {
648
649
  html += `<td>Last OK ${this._formatDuration(row['last-success'])} ago</td>\n`;
649
650
  }
@@ -673,20 +674,20 @@ class RegistryAPI {
673
674
  const data = this.crawler.getData();
674
675
  let html = '<table class="grid">';
675
676
 
676
- html += `<tr><td width="130px"><img src="/assets/images/tx-registry-root.gif">&nbsp;Registries</td><td>${data.address} (${this._escapeHtml(data.outcome)})</td></tr>`;
677
+ html += `<tr><td width="130px"><img src="/assets/images/tx-registry-root.gif">&nbsp;Registries</td><td>${data.address} (${escape(data.outcome)})</td></tr>`;
677
678
 
678
679
  data.registries.forEach(registry => {
679
680
  if (registry.error) {
680
- html += `<tr><td title="${this._escapeHtml(registry.name)}">&nbsp;<img src="/assets/images/tx-registry.png">&nbsp;${registry.code}</td><td><a href="${this._escapeHtml(registry.address)}">${this._escapeHtml(registry.address)}</a>. Error: ${this._escapeHtml(registry.error)}</td></tr>`;
681
+ html += `<tr><td title="${escape(registry.name)}">&nbsp;<img src="/assets/images/tx-registry.png">&nbsp;${registry.code}</td><td><a href="${escape(registry.address)}">${escape(registry.address)}</a>. Error: ${escape(registry.error)}</td></tr>`;
681
682
  } else {
682
- html += `<tr><td title="${this._escapeHtml(registry.name)}">&nbsp;&nbsp;<img src="/assets/images/tx-registry.png">&nbsp;${registry.code}</td><td><a href="${this._escapeHtml(registry.address)}">${this._escapeHtml(registry.address)}</a></td></tr>`;
683
+ html += `<tr><td title="${escape(registry.name)}">&nbsp;&nbsp;<img src="/assets/images/tx-registry.png">&nbsp;${registry.code}</td><td><a href="${escape(registry.address)}">${escape(registry.address)}</a></td></tr>`;
683
684
  }
684
685
 
685
686
  registry.servers.forEach(server => {
686
687
  if (server.authCSList.length > 0 || server.authVSList.length > 0 || server.usageList.length > 0) {
687
- html += `<tr><td title="${this._escapeHtml(server.name)}">&nbsp;&nbsp;&nbsp;&nbsp;<img src="/assets/images/tx-server.png">&nbsp;${server.code}</td><td><a href="${this._escapeHtml(server.address)}">${this._escapeHtml(server.address)}</a>. ${server.description}</td></tr>`;
688
+ html += `<tr><td title="${escape(server.name)}">&nbsp;&nbsp;&nbsp;&nbsp;<img src="/assets/images/tx-server.png">&nbsp;${server.code}</td><td><a href="${escape(server.address)}">${escape(server.address)}</a>. ${server.description}</td></tr>`;
688
689
  } else {
689
- html += `<tr><td title="${this._escapeHtml(server.name)}">&nbsp;&nbsp;&nbsp;&nbsp;<img src="/assets/images/tx-server.png">&nbsp;${server.code}</td><td><a href="${this._escapeHtml(server.address)}">${this._escapeHtml(server.address)}</a></td></tr>`;
690
+ html += `<tr><td title="${escape(server.name)}">&nbsp;&nbsp;&nbsp;&nbsp;<img src="/assets/images/tx-server.png">&nbsp;${server.code}</td><td><a href="${escape(server.address)}">${escape(server.address)}</a></td></tr>`;
690
691
  }
691
692
 
692
693
  server.versions.forEach(version => {
@@ -694,7 +695,7 @@ class RegistryAPI {
694
695
  const versionParts = version.version.split('.');
695
696
  const majorMinor = versionParts.slice(0, 2).join('.');
696
697
 
697
- html += `<tr><td>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<img src="/assets/images/tx-version.png">&nbsp;v${majorMinor}</td><td><a href="${this._escapeHtml(version.address)}">${this._escapeHtml(version.address)}</a>. Status: ${this._escapeHtml(version.details)}. ${version.codeSystems.length} CodeSystems, ${version.valueSets.length} ValueSets</td></tr>`;
698
+ html += `<tr><td>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<img src="/assets/images/tx-version.png">&nbsp;v${majorMinor}</td><td><a href="${escape(version.address)}">${escape(version.address)}</a>. Status: ${escape(version.details)}. ${version.codeSystems.length} CodeSystems, ${version.valueSets.length} ValueSets</td></tr>`;
698
699
  });
699
700
  });
700
701
  });
@@ -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,
@@ -195,17 +195,7 @@ npm run test:jest
195
195
 
196
196
  ## Data Persistence
197
197
 
198
- The crawler can save and load its state:
199
-
200
- ```javascript
201
- // Save current state
202
- const data = crawler.saveData();
203
- fs.writeFileSync('registry-data.json', JSON.stringify(data));
204
-
205
- // Load saved state
206
- const savedData = JSON.parse(fs.readFileSync('registry-data.json'));
207
- crawler.loadData(savedData);
208
- ```
198
+ The crawler saves and loads its state in [data]/registry-data.json
209
199
 
210
200
  ## Development
211
201
 
@@ -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
@@ -11,6 +11,8 @@ const express = require('express');
11
11
  const path = require('path');
12
12
  const fs = require('fs');
13
13
  const folders = require('./library/folder-setup'); // <-- ADD: load early
14
+ const { statSync, readdirSync } = require('fs');
15
+ const escape = require('escape-html');
14
16
 
15
17
  // Load configuration BEFORE logger
16
18
  let config;
@@ -46,7 +48,7 @@ const packageJson = require('./package.json');
46
48
  const htmlServer = require('./library/html-server');
47
49
  const ServerStats = require("./stats");
48
50
  const {Liquid} = require("liquidjs");
49
- const {escapeHtml} = require("./library/utilities");
51
+
50
52
  htmlServer.useLog(serverLog);
51
53
 
52
54
  const app = express();
@@ -182,7 +184,6 @@ async function initializeModules() {
182
184
  modules.tx = new TXModule(stats);
183
185
  await modules.tx.initialize(config.modules.tx, app);
184
186
  } catch (error) {
185
- console.log(error);
186
187
  serverLog.error('Failed to initialize TX module:', error);
187
188
  throw error;
188
189
  }
@@ -333,7 +334,7 @@ async function buildRootPageContent() {
333
334
  content += '<table class="grid">';
334
335
  content += '<tr>';
335
336
  content += `<td><strong>Module Count:</strong> ${mc}</td>`;
336
- content += `<td><strong>Uptime:</strong> ${escapeHtml(uptimeStr)}</td>`;
337
+ content += `<td><strong>Uptime:</strong> ${escape(uptimeStr)}</td>`;
337
338
  content += `<td><strong>Request Count:</strong> ${stats.requestCount}</td>`;
338
339
  content += '</tr>';
339
340
  content += '<tr>';
@@ -341,7 +342,7 @@ async function buildRootPageContent() {
341
342
  content += `<td><strong>Heap Total:</strong> ${heapTotalMB} MB</td>`;
342
343
  content += `<td><strong>Process Memory:</strong> ${rssMB} MB</td>`;
343
344
  content += '</tr>';
344
-
345
+ content += getLogStats();
345
346
  content += '</table>';
346
347
 
347
348
 
@@ -355,6 +356,7 @@ async function buildRootPageContent() {
355
356
  historyJson: JSON.stringify(stats.history),
356
357
  startTime: stats.startTime
357
358
  });
359
+ content += stats.taskDetails();
358
360
 
359
361
  content += '</div>';
360
362
  return content;
@@ -388,7 +390,7 @@ app.get('/', async (req, res) => {
388
390
  processingTime: Date.now() - startTime
389
391
  };
390
392
 
391
- const html = htmlServer.renderPage('root', config.hostName || 'FHIRsmith Server', content, stats);
393
+ const html = htmlServer.renderPage('root', escape(config.hostName) || 'FHIRsmith Server', content, stats);
392
394
  res.setHeader('Content-Type', 'text/html');
393
395
  res.send(html);
394
396
  } catch (error) {
@@ -473,6 +475,52 @@ app.get('/health', async (req, res) => {
473
475
  res.json(healthStatus);
474
476
  });
475
477
 
478
+ /**
479
+ * Get log directory statistics: file count, total size, and disk space info
480
+ * @returns {string} HTML table row(s) with log stats
481
+ */
482
+ function getLogStats() {
483
+ const logDir = folders.logsDir();
484
+
485
+ try {
486
+ const files = readdirSync(logDir).filter(f => f.endsWith('.log'));
487
+ let totalSize = 0;
488
+ for (const file of files) {
489
+ totalSize += statSync(path.join(logDir, file)).size;
490
+ }
491
+
492
+ const sizeMB = (totalSize / 1024 / 1024).toFixed(2);
493
+
494
+ let diskInfo = '';
495
+ try {
496
+ // statfs available in Node 18.15+
497
+ const stats = fs.statfsSync(logDir);
498
+ const blockSize = stats.bsize;
499
+ const freeSpace = stats.bavail * blockSize;
500
+ const totalSpace = stats.blocks * blockSize;
501
+ const freeGB = (freeSpace / 1024 / 1024 / 1024).toFixed(2);
502
+ const totalGB = (totalSpace / 1024 / 1024 / 1024).toFixed(2);
503
+ diskInfo = `<td><strong>Disk Space:</strong> ${freeGB} GB of ${totalGB} GB</td>`;
504
+ } catch {
505
+ diskInfo = '<td><strong>Disk Space:</strong> unavailable</td>';
506
+ }
507
+
508
+ // Log rotation limit from logger config
509
+ const loggerOpts = Logger.getInstance().options;
510
+ const maxFiles = loggerOpts.file.maxFiles;
511
+ const maxSize = loggerOpts.file.maxSize;
512
+ const limitInfo = `${maxFiles} files × ${maxSize} each`;
513
+
514
+ return '<tr>'
515
+ + `<td><strong>Existing Logs:</strong> ${files.length} (${sizeMB} MB)</td>`
516
+ + `<td><strong>Retention Policy:</strong> ${limitInfo}</td>`
517
+ + diskInfo
518
+ + '</tr>';
519
+ } catch (e) {
520
+ return `<tr><td colspan="3"><strong>Logs:</strong> unable to read (${e.message})</td></tr>`;
521
+ }
522
+ }
523
+
476
524
  // Initialize everything
477
525
  async function startServer() {
478
526
  try {
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
  {