fhirsmith 0.4.2 → 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 (92) hide show
  1. package/CHANGELOG.md +12 -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 +53 -5
  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 +108 -99
package/xig/xig.js CHANGED
@@ -15,8 +15,10 @@ const { EventEmitter } = require('events');
15
15
  const zlib = require('zlib');
16
16
  const htmlServer = require('../library/html-server');
17
17
  const folders = require('../library/folder-setup');
18
+ const escape = require('escape-html');
18
19
 
19
20
  const Logger = require('../library/logger');
21
+ const {describeCron} = require("../library/cron-utilities");
20
22
  const xigLog = Logger.getInstance().child({ module: 'xig' });
21
23
 
22
24
  const router = express.Router();
@@ -69,26 +71,6 @@ function getUpdateHistory() {
69
71
  return updateHistory;
70
72
  }
71
73
 
72
- // Enhanced HTML escaping
73
- function escapeHtml(text) {
74
- if (typeof text !== 'string') {
75
- return String(text);
76
- }
77
-
78
- const map = {
79
- '&': '&',
80
- '<': '&lt;',
81
- '>': '&gt;',
82
- '"': '&quot;',
83
- "'": '&#x27;',
84
- '/': '&#x2F;',
85
- '`': '&#x60;',
86
- '=': '&#x3D;'
87
- };
88
-
89
- return text.replace(/[&<>"'`=/]/g, function(m) { return map[m]; });
90
- }
91
-
92
74
  // URL validation for external requests
93
75
  function validateExternalUrl(url) {
94
76
  try {
@@ -421,13 +403,13 @@ function buildContentHtml(contentData) {
421
403
  let html = '';
422
404
 
423
405
  if (contentData.message) {
424
- html += `<p>${escapeHtml(contentData.message)}</p>`;
406
+ html += `<p>${escape(contentData.message)}</p>`;
425
407
  }
426
408
 
427
409
  if (contentData.data && Array.isArray(contentData.data)) {
428
410
  html += '<ul>';
429
411
  contentData.data.forEach(item => {
430
- html += `<li>${escapeHtml(item)}</li>`;
412
+ html += `<li>${escape(item)}</li>`;
431
413
  });
432
414
  html += '</ul>';
433
415
  }
@@ -437,7 +419,7 @@ function buildContentHtml(contentData) {
437
419
  if (contentData.table.headers) {
438
420
  html += '<thead><tr>';
439
421
  contentData.table.headers.forEach(header => {
440
- html += `<th>${escapeHtml(header)}</th>`;
422
+ html += `<th>${escape(header)}</th>`;
441
423
  });
442
424
  html += '</tr></thead>';
443
425
  }
@@ -446,7 +428,7 @@ function buildContentHtml(contentData) {
446
428
  contentData.table.rows.forEach(row => {
447
429
  html += '<tr>';
448
430
  row.forEach(cell => {
449
- html += `<td>${escapeHtml(cell)}</td>`;
431
+ html += `<td>${escape(cell)}</td>`;
450
432
  });
451
433
  html += '</tr>';
452
434
  });
@@ -712,9 +694,9 @@ function renderExtension(details) {
712
694
  const modifier = parts[1] || '';
713
695
  const type = parts[2] || '';
714
696
 
715
- return `<td>${escapeHtml(context)}</td><td>${escapeHtml(modifier)}</td><td>${escapeHtml(type)}</td>`;
697
+ return `<td>${escape(context)}</td><td>${escape(modifier)}</td><td>${escape(type)}</td>`;
716
698
  } catch (error) {
717
- return `<td colspan="3">${escapeHtml(details)}</td>`;
699
+ return `<td colspan="3">${escape(details)}</td>`;
718
700
  }
719
701
  }
720
702
 
@@ -841,16 +823,16 @@ async function buildResourceTable(queryParams, resourceCount, offset = 0) {
841
823
  // Package column
842
824
  const packageObj = getPackage(row.PackageKey);
843
825
  if (packageObj && packageObj.Web) {
844
- parts.push(`<td><a href="${escapeHtml(packageObj.Web)}" target="_blank">${escapeHtml(packageObj.Id)}</a></td>`);
826
+ parts.push(`<td><a href="${escape(packageObj.Web)}" target="_blank">${escape(packageObj.Id)}</a></td>`);
845
827
  } else if (packageObj) {
846
- parts.push(`<td>${escapeHtml(packageObj.Id)}</td>`);
828
+ parts.push(`<td>${escape(packageObj.Id)}</td>`);
847
829
  } else {
848
- parts.push(`<td>Package ${escapeHtml(String(row.PackageKey))}</td>`);
830
+ parts.push(`<td>Package ${escape(String(row.PackageKey))}</td>`);
849
831
  }
850
832
 
851
833
  // Version column (if not filtered)
852
834
  if (!ver || ver === '') {
853
- parts.push(`<td>${escapeHtml(showVersion(row))}</td>`);
835
+ parts.push(`<td>${escape(showVersion(row))}</td>`);
854
836
  }
855
837
 
856
838
  // Identity column with complex link logic
@@ -864,53 +846,53 @@ async function buildResourceTable(queryParams, resourceCount, offset = 0) {
864
846
  }
865
847
 
866
848
  const identityText = (row.ResourceType + '/').replace(resourceTypePrefix, '') + row.Id;
867
- parts.push(`<td><a href="${identityLink}">${escapeHtml(identityText)}</a></td>`);
849
+ parts.push(`<td><a href="${identityLink}">${escape(identityText)}</a></td>`);
868
850
 
869
851
  // Name/Title column
870
852
  const displayName = row.Title || row.Name || '';
871
- parts.push(`<td>${escapeHtml(displayName)}</td>`);
853
+ parts.push(`<td>${escape(displayName)}</td>`);
872
854
 
873
855
  // Status column
874
856
  if (row.StandardsStatus) {
875
- parts.push(`<td>${escapeHtml(row.StandardsStatus || '')}</td>`);
857
+ parts.push(`<td>${escape(row.StandardsStatus || '')}</td>`);
876
858
  } else {
877
- parts.push(`<td>${escapeHtml(row.Status || '')}</td>`);
859
+ parts.push(`<td>${escape(row.Status || '')}</td>`);
878
860
  }
879
861
 
880
862
  // FMM/WG Columns
881
- parts.push(`<td>${escapeHtml(row.FMM || '')}</td>`);
882
- parts.push(`<td>${escapeHtml(row.WG || '')}</td>`);
863
+ parts.push(`<td>${escape(row.FMM || '')}</td>`);
864
+ parts.push(`<td>${escape(row.WG || '')}</td>`);
883
865
 
884
866
  // Date column
885
867
  parts.push(`<td>${formatDate(row.Date)}</td>`);
886
868
 
887
869
  // Realm column (if not filtered)
888
870
  if (!realm || realm === '') {
889
- parts.push(`<td>${escapeHtml(row.Realm || '')}</td>`);
871
+ parts.push(`<td>${escape(row.Realm || '')}</td>`);
890
872
  }
891
873
 
892
874
  // Authority column (if not filtered)
893
875
  if (!auth || auth === '') {
894
- parts.push(`<td>${escapeHtml(row.Authority || '')}</td>`);
876
+ parts.push(`<td>${escape(row.Authority || '')}</td>`);
895
877
  }
896
878
 
897
879
  // Type-specific columns
898
880
  switch (type) {
899
881
  case 'cs': // CodeSystem
900
882
  if (row.Supplements && row.Supplements !== '') {
901
- parts.push(`<td>Suppl: ${escapeHtml(row.Supplements)}</td>`);
883
+ parts.push(`<td>Suppl: ${escape(row.Supplements)}</td>`);
902
884
  } else {
903
- parts.push(`<td>${escapeHtml(row.Content || '')}</td>`);
885
+ parts.push(`<td>${escape(row.Content || '')}</td>`);
904
886
  }
905
887
  break;
906
888
  case 'rp': // Resource Profiles
907
889
  if (!rt || rt === '') {
908
- parts.push(`<td>${escapeHtml(row.Type || '')}</td>`);
890
+ parts.push(`<td>${escape(row.Type || '')}</td>`);
909
891
  }
910
892
  break;
911
893
  case 'dp': // Datatype Profiles
912
894
  if (!rt || rt === '') {
913
- parts.push(`<td>${escapeHtml(row.Type || '')}</td>`);
895
+ parts.push(`<td>${escape(row.Type || '')}</td>`);
914
896
  }
915
897
  break;
916
898
  case 'ext': // Extensions
@@ -919,13 +901,13 @@ async function buildResourceTable(queryParams, resourceCount, offset = 0) {
919
901
  case 'vs': // ValueSets
920
902
  case 'cm': { // ConceptMaps
921
903
  const details = (row.Details || '').replace(/,/g, ' ');
922
- parts.push(`<td>${escapeHtml(details)}</td>`);
904
+ parts.push(`<td>${escape(details)}</td>`);
923
905
  break;
924
906
  }
925
907
  case 'lm': { // Logical Models
926
908
  const packageCanonical = packageObj ? packageObj.Canonical : '';
927
909
  const typeText = (row.Type || '').replace(packageCanonical + 'StructureDefinition/', '');
928
- parts.push(`<td>${escapeHtml(typeText)}</td>`);
910
+ parts.push(`<td>${escape(typeText)}</td>`);
929
911
  break;
930
912
  }
931
913
  }
@@ -940,7 +922,7 @@ async function buildResourceTable(queryParams, resourceCount, offset = 0) {
940
922
 
941
923
  } catch (error) {
942
924
  xigLog.error(`Error building resource table: ${error.message}`);
943
- return `<p class="text-danger">Error loading resource list: ${escapeHtml(error.message)}</p>`;
925
+ return `<p class="text-danger">Error loading resource list: ${escape(error.message)}</p>`;
944
926
  }
945
927
  }
946
928
 
@@ -980,9 +962,9 @@ async function buildSummaryStats(queryParams, baseUrl) {
980
962
  });
981
963
 
982
964
  const linkUrl = buildVersionLinkUrl(baseUrl, queryParams, version);
983
- html += `<li><a href="${linkUrl}">${escapeHtml(version)}</a>: ${count.toLocaleString()}</li>`;
965
+ html += `<li><a href="${linkUrl}">${escape(version)}</a>: ${count.toLocaleString()}</li>`;
984
966
  } catch (error) {
985
- html += `<li>${escapeHtml(version)}: Error</li>`;
967
+ html += `<li>${escape(version)}: Error</li>`;
986
968
  }
987
969
  }
988
970
  html += '</ul>';
@@ -1015,7 +997,7 @@ async function buildSummaryStats(queryParams, baseUrl) {
1015
997
  html += `<li>none: ${count.toLocaleString()}</li>`;
1016
998
  } else {
1017
999
  const linkUrl = buildAuthorityLinkUrl(baseUrl, queryParams, authority);
1018
- html += `<li><a href="${linkUrl}">${escapeHtml(authority)}</a>: ${count.toLocaleString()}</li>`;
1000
+ html += `<li><a href="${linkUrl}">${escape(authority)}</a>: ${count.toLocaleString()}</li>`;
1019
1001
  }
1020
1002
  });
1021
1003
  html += '</ul>';
@@ -1051,7 +1033,7 @@ async function buildSummaryStats(queryParams, baseUrl) {
1051
1033
  c++;
1052
1034
  } else {
1053
1035
  const linkUrl = buildRealmLinkUrl(baseUrl, queryParams, realmCode);
1054
- html += `<li><a href="${linkUrl}">${escapeHtml(realmCode)}</a>: ${count.toLocaleString()}</li>`;
1036
+ html += `<li><a href="${linkUrl}">${escape(realmCode)}</a>: ${count.toLocaleString()}</li>`;
1055
1037
  }
1056
1038
  });
1057
1039
  if (c > 0) {
@@ -1064,7 +1046,7 @@ async function buildSummaryStats(queryParams, baseUrl) {
1064
1046
  } catch (error) {
1065
1047
  console.error(error);
1066
1048
  xigLog.error(`Error building summary stats: ${error.message}`);
1067
- html += `<p class="text-warning">Error loading summary statistics: ${escapeHtml(error.message)}</p>`;
1049
+ html += `<p class="text-warning">Error loading summary statistics: ${escape(error.message)}</p>`;
1068
1050
  }
1069
1051
 
1070
1052
  return html;
@@ -1123,9 +1105,9 @@ function makeSelect(selectedValue, optionsList, name = 'rt') {
1123
1105
  }
1124
1106
 
1125
1107
  if (selectedValue === code) {
1126
- html += `<option value="${escapeHtml(code)}" selected="true">${escapeHtml(display)}</option>`;
1108
+ html += `<option value="${escape(code)}" selected="true">${escape(display)}</option>`;
1127
1109
  } else {
1128
- html += `<option value="${escapeHtml(code)}">${escapeHtml(display)}</option>`;
1110
+ html += `<option value="${escape(code)}">${escape(display)}</option>`;
1129
1111
  }
1130
1112
  });
1131
1113
 
@@ -1140,13 +1122,13 @@ function buildAdditionalForm(queryParams) {
1140
1122
 
1141
1123
  // Add hidden inputs to preserve current filter state
1142
1124
  if (ver && ver !== '') {
1143
- html += `<input type="hidden" name="ver" value="${escapeHtml(ver)}"/>`;
1125
+ html += `<input type="hidden" name="ver" value="${escape(ver)}"/>`;
1144
1126
  }
1145
1127
  if (realm && realm !== '') {
1146
- html += `<input type="hidden" name="realm" value="${escapeHtml(realm)}"/>`;
1128
+ html += `<input type="hidden" name="realm" value="${escape(realm)}"/>`;
1147
1129
  }
1148
1130
  if (auth && auth !== '') {
1149
- html += `<input type="hidden" name="auth" value="${escapeHtml(auth)}"/>`;
1131
+ html += `<input type="hidden" name="auth" value="${escape(auth)}"/>`;
1150
1132
  }
1151
1133
 
1152
1134
  // Add type-specific fields
@@ -1214,7 +1196,7 @@ function buildAdditionalForm(queryParams) {
1214
1196
  }
1215
1197
 
1216
1198
  // Add text search field
1217
- html += `Text: <input type="text" name="text" value="${escapeHtml(text || '')}" class="" style="width: 200px;"/> `;
1199
+ html += `Text: <input type="text" name="text" value="${escape(text || '')}" class="" style="width: 200px;"/> `;
1218
1200
 
1219
1201
  // Add submit button
1220
1202
  html += '<input type="submit" value="Search" style="color:rgb(89, 137, 241)"/>';
@@ -1275,7 +1257,7 @@ function buildPageHeading(queryParams) {
1275
1257
  default:
1276
1258
  // No type selected or unknown type
1277
1259
  if (rt && rt !== '') {
1278
- heading += `Resources - ${escapeHtml(rt)}`;
1260
+ heading += `Resources - ${escape(rt)}`;
1279
1261
  } else {
1280
1262
  heading += 'Resources - All Kinds';
1281
1263
  }
@@ -1284,15 +1266,15 @@ function buildPageHeading(queryParams) {
1284
1266
 
1285
1267
  // Add additional qualifiers
1286
1268
  if (realm && realm !== '') {
1287
- heading += `, Realm ${escapeHtml(realm.toUpperCase())}`;
1269
+ heading += `, Realm ${escape(realm.toUpperCase())}`;
1288
1270
  }
1289
1271
 
1290
1272
  if (auth && auth !== '') {
1291
- heading += `, Authority ${escapeHtml(capitalizeFirst(auth))}`;
1273
+ heading += `, Authority ${escape(capitalizeFirst(auth))}`;
1292
1274
  }
1293
1275
 
1294
1276
  if (ver && ver !== '') {
1295
- heading += `, Version ${escapeHtml(ver)}`;
1277
+ heading += `, Version ${escape(ver)}`;
1296
1278
  }
1297
1279
 
1298
1280
  heading += '</h2>';
@@ -1329,10 +1311,10 @@ function buildVersionBar(baseUrl, currentParams) {
1329
1311
  const versions = getCachedSet('versions');
1330
1312
  versions.forEach(version => {
1331
1313
  if (version === ver) {
1332
- html += ` | <b>${escapeHtml(version)}</b>`;
1314
+ html += ` | <b>${escape(version)}</b>`;
1333
1315
  } else {
1334
1316
  const separator = baseUrlWithoutVer.includes('?') ? '&' : '?';
1335
- html += ` | <a href="${baseUrlWithoutVer}${separator}ver=${encodeURIComponent(version)}">${escapeHtml(version)}</a>`;
1317
+ html += ` | <a href="${baseUrlWithoutVer}${separator}ver=${encodeURIComponent(version)}">${escape(version)}</a>`;
1336
1318
  }
1337
1319
  });
1338
1320
 
@@ -1356,10 +1338,10 @@ function buildAuthorityBar(baseUrl, currentParams) {
1356
1338
  const authorities = getCachedSet('authorities');
1357
1339
  authorities.forEach(authority => {
1358
1340
  if (authority === auth) {
1359
- html += ` | <b>${escapeHtml(authority)}</b>`;
1341
+ html += ` | <b>${escape(authority)}</b>`;
1360
1342
  } else {
1361
1343
  const separator = baseUrlWithoutAuth.includes('?') ? '&' : '?';
1362
- html += ` | <a href="${baseUrlWithoutAuth}${separator}auth=${encodeURIComponent(authority)}">${escapeHtml(authority)}</a>`;
1344
+ html += ` | <a href="${baseUrlWithoutAuth}${separator}auth=${encodeURIComponent(authority)}">${escape(authority)}</a>`;
1363
1345
  }
1364
1346
  });
1365
1347
 
@@ -1383,10 +1365,10 @@ function buildRealmBar(baseUrl, currentParams) {
1383
1365
  const realms = getCachedSet('realms');
1384
1366
  realms.forEach(realmCode => {
1385
1367
  if (realmCode === realm) {
1386
- html += ` | <b>${escapeHtml(realmCode)}</b>`;
1368
+ html += ` | <b>${escape(realmCode)}</b>`;
1387
1369
  } else {
1388
1370
  const separator = baseUrlWithoutRealm.includes('?') ? '&' : '?';
1389
- html += ` | <a href="${baseUrlWithoutRealm}${separator}realm=${encodeURIComponent(realmCode)}">${escapeHtml(realmCode)}</a>`;
1371
+ html += ` | <a href="${baseUrlWithoutRealm}${separator}realm=${encodeURIComponent(realmCode)}">${escape(realmCode)}</a>`;
1390
1372
  }
1391
1373
  });
1392
1374
 
@@ -1411,10 +1393,10 @@ function buildTypeBar(baseUrl, currentParams) {
1411
1393
  if (typesMap instanceof Map) {
1412
1394
  typesMap.forEach((display, code) => {
1413
1395
  if (code === type) {
1414
- html += ` | <b>${escapeHtml(display)}</b>`;
1396
+ html += ` | <b>${escape(display)}</b>`;
1415
1397
  } else {
1416
1398
  const separator = baseUrlWithoutType.includes('?') ? '&' : '?';
1417
- html += ` | <a href="${baseUrlWithoutType}${separator}type=${encodeURIComponent(code)}">${escapeHtml(display)}</a>`;
1399
+ html += ` | <a href="${baseUrlWithoutType}${separator}type=${encodeURIComponent(code)}">${escape(display)}</a>`;
1418
1400
  }
1419
1401
  });
1420
1402
  }
@@ -1519,6 +1501,9 @@ function getMetadata(key) {
1519
1501
  function downloadFile(url, destination, maxRedirects = 5) {
1520
1502
  return new Promise((resolve, reject) => {
1521
1503
  xigLog.info(`Starting download from ${url}`);
1504
+ if (globalStats) {
1505
+ globalStats.task('XIG Download', `Downloading from ${url}`)
1506
+ }
1522
1507
  const downloadMeta = {
1523
1508
  url: url,
1524
1509
  finalUrl: url,
@@ -1564,6 +1549,9 @@ function downloadFile(url, destination, maxRedirects = 5) {
1564
1549
  }
1565
1550
 
1566
1551
  if (response.statusCode !== 200) {
1552
+ if (globalStats) {
1553
+ globalStats.task('XIG Download', `Download failed: ${response.statusCode}`)
1554
+ }
1567
1555
  reject(Object.assign(
1568
1556
  new Error(`Download failed with HTTP ${response.statusCode}`),
1569
1557
  { downloadMeta }
@@ -1574,6 +1562,9 @@ function downloadFile(url, destination, maxRedirects = 5) {
1574
1562
  // Check content length
1575
1563
  const maxSize = 10 * 1024 * 1024 * 1024; // 10GB limit
1576
1564
  if (downloadMeta.contentLength && downloadMeta.contentLength > maxSize) {
1565
+ if (globalStats) {
1566
+ globalStats.task('XIG Download', `Download failed: too large`)
1567
+ }
1577
1568
  reject(Object.assign(new Error('File too large'), { downloadMeta }));
1578
1569
  return;
1579
1570
  }
@@ -1584,6 +1575,9 @@ function downloadFile(url, destination, maxRedirects = 5) {
1584
1575
  downloadMeta.downloadedBytes += chunk.length;
1585
1576
  if (downloadMeta.downloadedBytes > maxSize) {
1586
1577
  request.destroy();
1578
+ if (globalStats) {
1579
+ globalStats.task('XIG Download', `Download failed: file too large`);
1580
+ }
1587
1581
  fs.unlink(destination, () => {}); // Clean up
1588
1582
  reject(Object.assign(new Error('File too large'), { downloadMeta }));
1589
1583
  return;
@@ -1596,21 +1590,33 @@ function downloadFile(url, destination, maxRedirects = 5) {
1596
1590
  fileStream.close();
1597
1591
  downloadMeta.durationMs = Date.now() - downloadMeta.startTime;
1598
1592
  xigLog.info(`Download completed successfully. Downloaded ${downloadMeta.downloadedBytes} bytes to ${destination}`);
1593
+ if (globalStats) {
1594
+ globalStats.task('XIG Download', `Downloaded ${downloadMeta.downloadedBytes} bytes to ${destination}`);
1595
+ }
1599
1596
  resolve(downloadMeta);
1600
1597
  });
1601
1598
 
1602
1599
  fileStream.on('error', (err) => {
1600
+ if (globalStats) {
1601
+ globalStats.task('XIG Download', `Download failed`);
1602
+ }
1603
1603
  fs.unlink(destination, () => {}); // Delete partial file
1604
1604
  reject(Object.assign(err, { downloadMeta }));
1605
1605
  });
1606
1606
  });
1607
1607
 
1608
1608
  request.on('error', (err) => {
1609
+ if (globalStats) {
1610
+ globalStats.task('XIG Download', `Download Error`);
1611
+ }
1609
1612
  reject(Object.assign(err, { downloadMeta }));
1610
1613
  });
1611
1614
 
1612
1615
  request.setTimeout(300000, () => { // 5 minutes timeout
1613
1616
  request.destroy();
1617
+ if (globalStats) {
1618
+ globalStats.task('XIG Download', `Download Timeout`);
1619
+ }
1614
1620
  reject(Object.assign(new Error('Download timeout after 5 minutes'), { downloadMeta }));
1615
1621
  });
1616
1622
 
@@ -2037,7 +2043,7 @@ function buildStatsTable(statsData) {
2037
2043
  Object.keys(statsData.cache.tables).forEach(tableName => {
2038
2044
  const tableInfo = statsData.cache.tables[tableName];
2039
2045
  html += `<tr>`;
2040
- html += `<td>Cache: ${escapeHtml(tableName)}</td>`;
2046
+ html += `<td>Cache: ${escape(tableName)}</td>`;
2041
2047
  html += `<td>${tableInfo.size.toLocaleString()}</td>`;
2042
2048
  html += `<td>${tableInfo.type}</td>`;
2043
2049
  html += `</tr>`;
@@ -2058,12 +2064,12 @@ function buildStatsTable(statsData) {
2058
2064
  html += `<tr>`;
2059
2065
  html += `<td>Database File</td>`;
2060
2066
  html += `<td>${(statsData.database.fileSize / 1024 / 1024).toFixed(2)} MB</td>`;
2061
- html += `<td>${escapeHtml(XIG_DB_PATH)}</td>`;
2067
+ html += `<td>${escape(XIG_DB_PATH)}</td>`;
2062
2068
  html += `</tr>`;
2063
2069
 
2064
2070
  html += `<tr>`;
2065
2071
  html += `<td>Download Source</td>`;
2066
- html += `<td colspan="2"><code>${escapeHtml(XIG_DB_URL)}</code></td>`;
2072
+ html += `<td colspan="2"><code>${escape(XIG_DB_URL)}</code></td>`;
2067
2073
  html += `</tr>`;
2068
2074
 
2069
2075
  html += `<tr>`;
@@ -2114,13 +2120,13 @@ function buildStatsTable(statsData) {
2114
2120
  detail += ` (HTTP ${entry.downloadMeta.httpStatus})`;
2115
2121
  }
2116
2122
  } else if (entry.status === 'failed') {
2117
- detail = escapeHtml(entry.error || 'Unknown error');
2123
+ detail = escape(entry.error || 'Unknown error');
2118
2124
  if (entry.downloadMeta) {
2119
2125
  if (entry.downloadMeta.httpStatus) {
2120
2126
  detail += ` (HTTP ${entry.downloadMeta.httpStatus})`;
2121
2127
  }
2122
2128
  if (entry.downloadMeta.finalUrl !== entry.sourceUrl) {
2123
- detail += `<br>Redirected to: <code>${escapeHtml(entry.downloadMeta.finalUrl)}</code>`;
2129
+ detail += `<br>Redirected to: <code>${escape(entry.downloadMeta.finalUrl)}</code>`;
2124
2130
  }
2125
2131
  if (entry.downloadMeta.downloadedBytes > 0) {
2126
2132
  detail += `<br>Partial download: ${(entry.downloadMeta.downloadedBytes / 1024 / 1024).toFixed(1)} MB`;
@@ -2283,7 +2289,7 @@ router.get('/', async (req, res) => {
2283
2289
  // Build resource count paragraph
2284
2290
  let countParagraph = '<p>';
2285
2291
  if (countError) {
2286
- countParagraph += `<span class="text-warning">Unable to get resource count: ${escapeHtml(countError)}</span>`;
2292
+ countParagraph += `<span class="text-warning">Unable to get resource count: ${escape(countError)}</span>`;
2287
2293
  } else {
2288
2294
  countParagraph += `${resourceCount.toLocaleString()} resources`;
2289
2295
  }
@@ -2356,7 +2362,7 @@ router.get('/stats', async (req, res) => {
2356
2362
  if (lastAttempt) {
2357
2363
  if (lastAttempt.status === 'failed') {
2358
2364
  introContent += `<br><strong>Last update attempt failed</strong> at ${new Date(lastAttempt.timestamp).toLocaleString()}: `;
2359
- introContent += `${escapeHtml(lastAttempt.error || 'Unknown error')}`;
2365
+ introContent += `${escape(lastAttempt.error || 'Unknown error')}`;
2360
2366
  if (lastAttempt.downloadMeta && lastAttempt.downloadMeta.httpStatus) {
2361
2367
  introContent += ` (HTTP ${lastAttempt.downloadMeta.httpStatus})`;
2362
2368
  }
@@ -2372,7 +2378,7 @@ router.get('/stats', async (req, res) => {
2372
2378
  // DB is fresh but last attempt failed — still worth showing
2373
2379
  introContent += `<div class="alert alert-warning">`;
2374
2380
  introContent += `<strong>Last update attempt failed</strong> at ${new Date(lastAttempt.timestamp).toLocaleString()}: `;
2375
- introContent += `${escapeHtml(lastAttempt.error || 'Unknown error')}`;
2381
+ introContent += `${escape(lastAttempt.error || 'Unknown error')}`;
2376
2382
  introContent += `</div>`;
2377
2383
  }
2378
2384
 
@@ -2419,7 +2425,7 @@ router.get('/resource/:packagePid/:resourceType/:resourceId', async (req, res) =
2419
2425
  const packageObj = getPackageByPid(dbPackagePid);
2420
2426
  if (!packageObj) {
2421
2427
  return res.status(404).send(renderPage('Resource Not Found',
2422
- `<div class="alert alert-danger">Unknown Package: ${escapeHtml(packagePid)}</div>`));
2428
+ `<div class="alert alert-danger">Unknown Package: ${escape(packagePid)}</div>`));
2423
2429
  }
2424
2430
 
2425
2431
  // Get resource details
@@ -2437,7 +2443,7 @@ router.get('/resource/:packagePid/:resourceType/:resourceId', async (req, res) =
2437
2443
 
2438
2444
  if (!resourceData) {
2439
2445
  return res.status(404).send(renderPage('Resource Not Found',
2440
- `<div class="alert alert-danger">Unknown Resource: ${escapeHtml(resourceType)}/${escapeHtml(resourceId)} in package ${escapeHtml(packagePid)}</div>`));
2446
+ `<div class="alert alert-danger">Unknown Resource: ${escape(resourceType)}/${escape(resourceId)} in package ${escape(packagePid)}</div>`));
2441
2447
  }
2442
2448
 
2443
2449
  // Build the resource detail page
@@ -2491,7 +2497,7 @@ async function buildResourceDetailPage(packageObj, resourceData, secure = false)
2491
2497
 
2492
2498
  } catch (error) {
2493
2499
  xigLog.error(`Error building resource detail content: ${error.message}`);
2494
- html += `<div class="alert alert-warning">Error loading some content: ${escapeHtml(error.message)}</div>`;
2500
+ html += `<div class="alert alert-warning">Error loading some content: ${escape(error.message)}</div>`;
2495
2501
  }
2496
2502
 
2497
2503
  return html;
@@ -2503,28 +2509,28 @@ async function buildResourceMetadataTable(packageObj, resourceData) {
2503
2509
 
2504
2510
  // Package
2505
2511
  if (packageObj && packageObj.Web) {
2506
- html += `<tr><td><strong>Package</strong></td><td><a href="${escapeHtml(packageObj.Web)}" target="_blank">${escapeHtml(packageObj.Id)}</a></td></tr>`;
2512
+ html += `<tr><td><strong>Package</strong></td><td><a href="${escape(packageObj.Web)}" target="_blank">${escape(packageObj.Id)}</a></td></tr>`;
2507
2513
  } else if (packageObj) {
2508
- html += `<tr><td><strong>Package</strong></td><td>${escapeHtml(packageObj.Id)}</td></tr>`;
2514
+ html += `<tr><td><strong>Package</strong></td><td>${escape(packageObj.Id)}</td></tr>`;
2509
2515
  }
2510
2516
 
2511
2517
  // Type
2512
- html += `<tr><td><strong>Resource Type</strong></td><td>${escapeHtml(resourceData.ResourceType)}</td></tr>`;
2518
+ html += `<tr><td><strong>Resource Type</strong></td><td>${escape(resourceData.ResourceType)}</td></tr>`;
2513
2519
 
2514
2520
  // Id
2515
- html += `<tr><td><strong>Id</strong></td><td>${escapeHtml(resourceData.Id)}</td></tr>`;
2521
+ html += `<tr><td><strong>Id</strong></td><td>${escape(resourceData.Id)}</td></tr>`;
2516
2522
 
2517
2523
  // FHIR Versions
2518
2524
  const versions = showVersion(resourceData);
2519
2525
  if (versions.includes(',')) {
2520
- html += `<tr><td><strong>FHIR Versions</strong></td><td>${escapeHtml(versions)}</td></tr>`;
2526
+ html += `<tr><td><strong>FHIR Versions</strong></td><td>${escape(versions)}</td></tr>`;
2521
2527
  } else {
2522
- html += `<tr><td><strong>FHIR Version</strong></td><td>${escapeHtml(versions)}</td></tr>`;
2528
+ html += `<tr><td><strong>FHIR Version</strong></td><td>${escape(versions)}</td></tr>`;
2523
2529
  }
2524
2530
 
2525
2531
  // Source
2526
2532
  if (resourceData.Web) {
2527
- html += `<tr><td><strong>Source</strong></td><td><a href="${escapeHtml(resourceData.Web)}" target="_blank">${escapeHtml(resourceData.Web)}</a></td></tr>`;
2533
+ html += `<tr><td><strong>Source</strong></td><td><a href="${escape(resourceData.Web)}" target="_blank">${escape(resourceData.Web)}</a></td></tr>`;
2528
2534
  }
2529
2535
 
2530
2536
  // Add all other non-empty fields
@@ -2555,7 +2561,7 @@ async function buildResourceMetadataTable(packageObj, resourceData) {
2555
2561
  const expValue = value === '1' ? 'True' : 'False';
2556
2562
  html += `<tr><td><strong>${field.label}</strong></td><td>${expValue}</td></tr>`;
2557
2563
  } else {
2558
- html += `<tr><td><strong>${field.label}</strong></td><td>${escapeHtml(value)}</td></tr>`;
2564
+ html += `<tr><td><strong>${field.label}</strong></td><td>${escape(value)}</td></tr>`;
2559
2565
  }
2560
2566
  }
2561
2567
  });
@@ -2619,7 +2625,7 @@ async function buildResourceDependencies(resourceData, secure = false) {
2619
2625
  html += await buildExtensionExamplesSection(resourceData.Url);
2620
2626
  }
2621
2627
  } catch (error) {
2622
- html += `<div class="alert alert-warning">Error loading dependencies: ${escapeHtml(error.message)}</div>`;
2628
+ html += `<div class="alert alert-warning">Error loading dependencies: ${escape(error.message)}</div>`;
2623
2629
  }
2624
2630
 
2625
2631
  return html;
@@ -2662,8 +2668,8 @@ async function buildExtensionExamplesSection(resourceUrl) {
2662
2668
  const versionName = example.Version ? (versionMap[example.Version] || example.Version.toString()) : '';
2663
2669
 
2664
2670
  html += '<tr>';
2665
- html += `<td><a href="${escapeHtml(example.Url || '')}">${escapeHtml(example.Name || '')}</a></td>`;
2666
- html += `<td>${escapeHtml(versionName)}</td>`;
2671
+ html += `<td><a href="${escape(example.Url || '')}">${escape(example.Name || '')}</a></td>`;
2672
+ html += `<td>${escape(versionName)}</td>`;
2667
2673
  html += '</tr>';
2668
2674
  });
2669
2675
 
@@ -2673,7 +2679,7 @@ async function buildExtensionExamplesSection(resourceUrl) {
2673
2679
 
2674
2680
  } catch (error) {
2675
2681
  xigLog.error(`Error loading extension examples: ${error.message}`);
2676
- html += `<div class="alert alert-warning">Error loading extension examples: ${escapeHtml(error.message)}</div>`;
2682
+ html += `<div class="alert alert-warning">Error loading extension examples: ${escape(error.message)}</div>`;
2677
2683
  }
2678
2684
 
2679
2685
  return html;
@@ -2690,7 +2696,7 @@ function buildDependencyTable(dependencies) {
2690
2696
  }
2691
2697
  currentType = dep.ResourceType;
2692
2698
  html += '<table class="table table-bordered">';
2693
- html += `<tr style="background-color: #eeeeee"><td colspan="2"><strong>${escapeHtml(currentType)}</strong></td></tr>`;
2699
+ html += `<tr style="background-color: #eeeeee"><td colspan="2"><strong>${escape(currentType)}</strong></td></tr>`;
2694
2700
  }
2695
2701
 
2696
2702
  html += '<tr>';
@@ -2708,15 +2714,15 @@ function buildDependencyTable(dependencies) {
2708
2714
  const parts = displayUrl.split('/');
2709
2715
  displayUrl = parts[parts.length - 1];
2710
2716
  }
2711
- html += `<td><a href="${resourceUrl}">${escapeHtml(displayUrl)}</a></td>`;
2717
+ html += `<td><a href="${resourceUrl}">${escape(displayUrl)}</a></td>`;
2712
2718
  } else {
2713
2719
  const displayId = dep.ResourceType + '/' + dep.Id;
2714
- html += `<td><a href="${resourceUrl}">${escapeHtml(displayId)}</a></td>`;
2720
+ html += `<td><a href="${resourceUrl}">${escape(displayId)}</a></td>`;
2715
2721
  }
2716
2722
 
2717
2723
  // Title or Name
2718
2724
  const displayName = dep.Title || dep.Name || '';
2719
- html += `<td>${escapeHtml(displayName)}</td>`;
2725
+ html += `<td>${escape(displayName)}</td>`;
2720
2726
 
2721
2727
  html += '</tr>';
2722
2728
  });
@@ -2784,7 +2790,7 @@ async function buildResourceNarrative(resourceKey, packageObj) {
2784
2790
 
2785
2791
  } catch (error) {
2786
2792
  xigLog.error(`Error loading narrative: ${error.message}`);
2787
- html += `<div class="alert alert-warning">Error loading narrative: ${escapeHtml(error.message)}</div>`;
2793
+ html += `<div class="alert alert-warning">Error loading narrative: ${escape(error.message)}</div>`;
2788
2794
  }
2789
2795
 
2790
2796
  return html;
@@ -2833,12 +2839,12 @@ async function buildResourceSource(resourceKey) {
2833
2839
  const formattedJson = JSON.stringify(jsonData, null, 2);
2834
2840
 
2835
2841
  html += '<pre>';
2836
- html += escapeHtml(formattedJson);
2842
+ html += escape(formattedJson);
2837
2843
  html += '</pre>';
2838
2844
 
2839
2845
  } catch (error) {
2840
2846
  xigLog.error(`Error loading source: ${error.message}`);
2841
- html += `<div class="alert alert-warning">Error loading source: ${escapeHtml(error.message)}</div>`;
2847
+ html += `<div class="alert alert-warning">Error loading source: ${escape(error.message)}</div>`;
2842
2848
  }
2843
2849
 
2844
2850
  return html;
@@ -2937,6 +2943,9 @@ async function initializeXigModule(stats) {
2937
2943
  }, 5000);
2938
2944
  }
2939
2945
 
2946
+ if (globalStats) {
2947
+ globalStats.addTask('XIG Download', describeCron(this.config.crawler.schedule));
2948
+ }
2940
2949
  // Check if auto-update is enabled
2941
2950
  // Note: This assumes we're called only when XIG is enabled
2942
2951
  cron.schedule('0 2 * * *', () => {
@@ -2988,7 +2997,7 @@ module.exports = {
2988
2997
  // Template functions
2989
2998
  renderPage,
2990
2999
  buildContentHtml,
2991
- escapeHtml,
3000
+ escape,
2992
3001
  loadTemplate,
2993
3002
 
2994
3003
  // Control panel functions