fhirsmith 0.7.5 → 0.8.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.
- package/CHANGELOG.md +50 -0
- package/README.md +8 -0
- package/library/html.js +4 -0
- package/library/languages.js +10 -0
- package/package.json +1 -1
- package/packages/package-crawler.js +106 -51
- package/packages/packages.js +14 -0
- package/publisher/publisher.js +118 -28
- package/registry/registry.js +99 -91
- package/root-bare-template.html +92 -0
- package/security.md +32 -0
- package/server.js +99 -22
- package/stats.js +43 -10
- package/tx/README.md +6 -6
- package/tx/cs/cs-api.js +3 -0
- package/tx/cs/cs-api.md +285 -0
- package/tx/cs/cs-loinc.js +14 -2
- package/tx/cs/cs-rxnorm.js +14 -10
- package/tx/cs/cs-snomed.js +166 -5
- package/tx/html/dash-metrics.liquid +147 -0
- package/tx/importers/import-rxnorm.module.js +4 -30
- package/tx/importers/readme.md +3 -1
- package/tx/library/canonical-resource.js +8 -0
- package/tx/library/conceptmap.js +3 -1
- package/tx/library/designations.js +4 -8
- package/tx/library/renderer.js +9 -9
- package/tx/library.js +10 -4
- package/tx/ocl/cm-ocl.cjs +185 -65
- package/tx/ocl/cs-ocl.cjs +69 -50
- package/tx/ocl/jobs/background-queue.cjs +0 -8
- package/tx/ocl/mappers/concept-mapper.cjs +13 -3
- package/tx/ocl/shared/patches.cjs +1 -0
- package/tx/ocl/vs-ocl.cjs +137 -157
- package/tx/operation-context.js +3 -3
- package/tx/provider.js +4 -3
- package/tx/sct/structures.js +5 -0
- package/tx/tx-html.js +36 -9
- package/tx/tx.fhir.org.yml +1 -1
- package/tx/tx.js +34 -11
- package/tx/vs/vs-database.js +127 -6
- package/tx/vs/vs-vsac.js +98 -3
- package/tx/workers/search.js +2 -1
- package/tx/workers/translate.js +39 -14
- package/tx/workers/validate.js +3 -3
- package/utilities/dashboard.html +274 -0
- package/xig/xig.js +171 -9
package/registry/registry.js
CHANGED
|
@@ -21,7 +21,7 @@ class RegistryModule {
|
|
|
21
21
|
this.isInitialized = false;
|
|
22
22
|
this.lastCrawlTime = null;
|
|
23
23
|
this.crawlInProgress = false;
|
|
24
|
-
|
|
24
|
+
|
|
25
25
|
// Thread-safe data storage
|
|
26
26
|
this.currentData = null;
|
|
27
27
|
this.dataLock = false;
|
|
@@ -46,7 +46,7 @@ class RegistryModule {
|
|
|
46
46
|
|
|
47
47
|
this.crawler = new RegistryCrawler(crawlerConfig, this.stats);
|
|
48
48
|
this.crawler.useLog(regLog);
|
|
49
|
-
|
|
49
|
+
|
|
50
50
|
// Initialize API with crawler
|
|
51
51
|
this.api = new RegistryAPI(this.crawler);
|
|
52
52
|
|
|
@@ -79,13 +79,13 @@ class RegistryModule {
|
|
|
79
79
|
const dataPath = folders.ensureFilePath('registry', 'registry-data.json'); // <-- CHANGE
|
|
80
80
|
const data = await fs.readFile(dataPath, 'utf8');
|
|
81
81
|
const jsonData = JSON.parse(data);
|
|
82
|
-
|
|
82
|
+
|
|
83
83
|
// Thread-safe update
|
|
84
84
|
await this.updateData(() => {
|
|
85
85
|
this.crawler.loadData(jsonData);
|
|
86
86
|
this.currentData = this.crawler.getData();
|
|
87
87
|
});
|
|
88
|
-
|
|
88
|
+
|
|
89
89
|
this.logger.info('Loaded saved registry data');
|
|
90
90
|
} catch (error) {
|
|
91
91
|
this.logger.info('No saved registry data found, will fetch fresh data');
|
|
@@ -113,7 +113,7 @@ class RegistryModule {
|
|
|
113
113
|
*/
|
|
114
114
|
startPeriodicCrawl(intervalMinutes) {
|
|
115
115
|
const intervalMs = intervalMinutes * 60 * 1000;
|
|
116
|
-
|
|
116
|
+
|
|
117
117
|
// Run initial crawl after a short delay
|
|
118
118
|
setTimeout(() => {
|
|
119
119
|
this.performCrawl();
|
|
@@ -145,7 +145,7 @@ class RegistryModule {
|
|
|
145
145
|
try {
|
|
146
146
|
// Perform the crawl
|
|
147
147
|
const newData = await this.crawler.crawl(this.config.masterUrl);
|
|
148
|
-
|
|
148
|
+
|
|
149
149
|
// Thread-safe update of current data
|
|
150
150
|
await this.updateData(() => {
|
|
151
151
|
this.currentData = newData;
|
|
@@ -153,40 +153,30 @@ class RegistryModule {
|
|
|
153
153
|
|
|
154
154
|
this.lastCrawlTime = new Date();
|
|
155
155
|
const elapsed = Date.now() - startTime;
|
|
156
|
-
|
|
156
|
+
|
|
157
157
|
// Save to disk
|
|
158
158
|
await this.saveData();
|
|
159
|
-
|
|
159
|
+
|
|
160
160
|
// Get metadata
|
|
161
161
|
const metadata = this.crawler.getMetadata();
|
|
162
162
|
this.logger.info(`Crawl completed in ${(elapsed/1000).toFixed(1)}s. ` +
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
this.stats.
|
|
163
|
+
`Found ${newData.registries.length} registries, ` +
|
|
164
|
+
`${metadata.errors.length} errors, ` +
|
|
165
|
+
`downloaded ${this.crawler.formatBytes(metadata.totalBytes)}`);
|
|
166
|
+
this.stats.taskDone('TxRegistry', 'Crawling Finished');
|
|
167
167
|
} catch (error) {
|
|
168
168
|
this.logger.error('Crawl failed:', error);
|
|
169
|
-
this.stats.
|
|
169
|
+
this.stats.taskError('TxRegistry', 'Crawling Error: '+error.message);
|
|
170
170
|
} finally {
|
|
171
171
|
this.crawlInProgress = false;
|
|
172
172
|
}
|
|
173
173
|
}
|
|
174
174
|
|
|
175
175
|
/**
|
|
176
|
-
*
|
|
176
|
+
* Data update - no locking needed, Node.js is single-threaded
|
|
177
177
|
*/
|
|
178
178
|
async updateData(updateFn) {
|
|
179
|
-
|
|
180
|
-
while (this.dataLock) {
|
|
181
|
-
await new Promise(resolve => setTimeout(resolve, 10));
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
this.dataLock = true;
|
|
185
|
-
try {
|
|
186
|
-
updateFn();
|
|
187
|
-
} finally {
|
|
188
|
-
this.dataLock = false;
|
|
189
|
-
}
|
|
179
|
+
updateFn();
|
|
190
180
|
}
|
|
191
181
|
|
|
192
182
|
_normalizeQueryParams(query) {
|
|
@@ -248,12 +238,12 @@ class RegistryModule {
|
|
|
248
238
|
*/
|
|
249
239
|
renderHtmlPage(req, res, jsonResult, basePath, registry, server, fhirVersion, codeSystem, valueSet) {
|
|
250
240
|
// Generate path with query parameters
|
|
251
|
-
let
|
|
252
|
-
if (registry)
|
|
253
|
-
if (server)
|
|
254
|
-
if (fhirVersion)
|
|
255
|
-
if (codeSystem)
|
|
256
|
-
if (valueSet)
|
|
241
|
+
let pagePath = basePath;
|
|
242
|
+
if (registry) pagePath += `®istry=${encodeURIComponent(registry)}`;
|
|
243
|
+
if (server) pagePath += `&server=${encodeURIComponent(server)}`;
|
|
244
|
+
if (fhirVersion) pagePath += `&fhirVersion=${encodeURIComponent(fhirVersion)}`;
|
|
245
|
+
if (codeSystem) pagePath += `&url=${encodeURIComponent(codeSystem)}`;
|
|
246
|
+
if (valueSet) pagePath += `&valueSet=${encodeURIComponent(valueSet)}`;
|
|
257
247
|
|
|
258
248
|
// Get registry documentation and info
|
|
259
249
|
const data = this.api.getData();
|
|
@@ -264,7 +254,7 @@ class RegistryModule {
|
|
|
264
254
|
|
|
265
255
|
// Render matches table
|
|
266
256
|
const matchesTable = this.api.renderJsonToHtml(
|
|
267
|
-
jsonResult,
|
|
257
|
+
jsonResult, pagePath, registry, server, fhirVersion
|
|
268
258
|
);
|
|
269
259
|
|
|
270
260
|
// Render registry info
|
|
@@ -272,7 +262,7 @@ class RegistryModule {
|
|
|
272
262
|
|
|
273
263
|
// Assemble template variables
|
|
274
264
|
const templateVars = {
|
|
275
|
-
path,
|
|
265
|
+
path: pagePath,
|
|
276
266
|
matches: matchesTable,
|
|
277
267
|
count: jsonResult.results.length,
|
|
278
268
|
registry: registry || '',
|
|
@@ -288,7 +278,7 @@ class RegistryModule {
|
|
|
288
278
|
// Use HTML server to render the page
|
|
289
279
|
try {
|
|
290
280
|
if (!htmlServer.hasTemplate('registry')) {
|
|
291
|
-
const templatePath = path.join(__dirname, '
|
|
281
|
+
const templatePath = path.join(__dirname, 'registry-template.html');
|
|
292
282
|
htmlServer.loadTemplate('registry', templatePath);
|
|
293
283
|
}
|
|
294
284
|
|
|
@@ -303,7 +293,7 @@ class RegistryModule {
|
|
|
303
293
|
);
|
|
304
294
|
} catch (error) {
|
|
305
295
|
this.logger.error('Error rendering page:', error);
|
|
306
|
-
return `<html><body><h1>Error rendering page</h1><p>${error.message}</p></body></html>`;
|
|
296
|
+
return `<html><body><h1>Error rendering page</h1><p>${escape(error.message)}</p></body></html>`;
|
|
307
297
|
}
|
|
308
298
|
}
|
|
309
299
|
|
|
@@ -315,9 +305,9 @@ class RegistryModule {
|
|
|
315
305
|
if (this.crawlInProgress) {
|
|
316
306
|
return 'Scanning for updates now';
|
|
317
307
|
} else if (!this.lastCrawlTime) {
|
|
318
|
-
const nextScan = this.crawlInterval ?
|
|
308
|
+
const nextScan = this.crawlInterval ?
|
|
319
309
|
new Date(Date.now() + this.crawlInterval) : null;
|
|
320
|
-
|
|
310
|
+
|
|
321
311
|
if (nextScan) {
|
|
322
312
|
const timeUntil = this.describePeriod(nextScan - Date.now());
|
|
323
313
|
return `First Scan in ${timeUntil}`;
|
|
@@ -325,9 +315,9 @@ class RegistryModule {
|
|
|
325
315
|
return 'No automatic scanning configured';
|
|
326
316
|
}
|
|
327
317
|
} else {
|
|
328
|
-
const nextScan = this.crawlInterval ?
|
|
318
|
+
const nextScan = this.crawlInterval ?
|
|
329
319
|
new Date(this.lastCrawlTime.getTime() + (this.config.crawlInterval * 60 * 1000)) : null;
|
|
330
|
-
|
|
320
|
+
|
|
331
321
|
if (nextScan) {
|
|
332
322
|
const timeUntil = this.describePeriod(nextScan - Date.now());
|
|
333
323
|
const timeSince = this.describePeriod(Date.now() - this.lastCrawlTime);
|
|
@@ -345,7 +335,7 @@ class RegistryModule {
|
|
|
345
335
|
*/
|
|
346
336
|
describePeriod(milliseconds) {
|
|
347
337
|
const seconds = Math.floor(milliseconds / 1000);
|
|
348
|
-
|
|
338
|
+
|
|
349
339
|
if (seconds < 60) {
|
|
350
340
|
return `${seconds} seconds`;
|
|
351
341
|
} else if (seconds < 3600) {
|
|
@@ -364,56 +354,63 @@ class RegistryModule {
|
|
|
364
354
|
const start = Date.now();
|
|
365
355
|
try {
|
|
366
356
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
// Render HTML page
|
|
388
|
-
try {
|
|
389
|
-
const startTime = Date.now();
|
|
390
|
-
|
|
391
|
-
// Load template if needed
|
|
392
|
-
if (!htmlServer.hasTemplate('registry')) {
|
|
393
|
-
const templatePath = path.join(__dirname, 'registry-template.html');
|
|
394
|
-
htmlServer.loadTemplate('registry', templatePath);
|
|
357
|
+
const acceptsHtml = req.headers.accept && req.headers.accept.includes('text/html');
|
|
358
|
+
|
|
359
|
+
if (!acceptsHtml) {
|
|
360
|
+
// Return JSON overview
|
|
361
|
+
return res.json({
|
|
362
|
+
name: 'FHIR Terminology Server Registry',
|
|
363
|
+
description: 'Registry and discovery service for FHIR terminology servers',
|
|
364
|
+
endpoints: {
|
|
365
|
+
status: '/registry/api/status',
|
|
366
|
+
statistics: '/registry/api/stats',
|
|
367
|
+
registries: '/registry/api/registries',
|
|
368
|
+
queryCodeSystem: '/registry/api/query/codesystem',
|
|
369
|
+
queryValueSet: '/registry/api/query/valueset',
|
|
370
|
+
bestServer: '/registry/api/best-server/{type}',
|
|
371
|
+
errors: '/registry/api/errors'
|
|
372
|
+
},
|
|
373
|
+
documentation: 'https://github.com/your-org/fhir-registry'
|
|
374
|
+
});
|
|
395
375
|
}
|
|
396
376
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
stats.crawlInProgress = this.crawlInProgress;
|
|
401
|
-
stats.lastCrawl = this.lastCrawlTime;
|
|
377
|
+
// Render HTML page
|
|
378
|
+
try {
|
|
379
|
+
const startTime = Date.now();
|
|
402
380
|
|
|
403
|
-
|
|
404
|
-
'registry'
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
381
|
+
// Load template if needed
|
|
382
|
+
if (!htmlServer.hasTemplate('registry')) {
|
|
383
|
+
const templatePath = path.join(__dirname, 'registry-template.html');
|
|
384
|
+
try {
|
|
385
|
+
htmlServer.loadTemplate('registry', templatePath);
|
|
386
|
+
} catch (templateError) {
|
|
387
|
+
this.logger.error('Failed to load registry template:', templateError);
|
|
388
|
+
return res.status(500).send(`<html><body><h1>Template Error</h1><p>Could not load registry-template.html: ${escape(templateError.message)}</p></body></html>`);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const content = await this.buildHtmlContent();
|
|
393
|
+
this.logger.info('Registry: buildHtmlContent completed');
|
|
394
|
+
const stats = this.api.getStatistics();
|
|
395
|
+
this.logger.info('Registry: getStatistics completed');
|
|
396
|
+
stats.processingTime = Date.now() - startTime;
|
|
397
|
+
stats.crawlInProgress = this.crawlInProgress;
|
|
398
|
+
stats.lastCrawl = this.lastCrawlTime;
|
|
399
|
+
|
|
400
|
+
const html = htmlServer.renderPage(
|
|
401
|
+
'registry',
|
|
402
|
+
'FHIR Terminology Server Registry',
|
|
403
|
+
content,
|
|
404
|
+
stats
|
|
405
|
+
);
|
|
406
|
+
|
|
407
|
+
res.setHeader('Content-Type', 'text/html');
|
|
408
|
+
res.send(html);
|
|
409
|
+
|
|
410
|
+
} catch (error) {
|
|
411
|
+
this.logger.error('Error rendering registry page:', error);
|
|
412
|
+
res.status(500).send(`<html><body><h1>Error rendering registry page</h1><pre>${escape(error.message)}\n${escape(error.stack || '')}</pre></body></html>`);
|
|
413
|
+
}
|
|
417
414
|
} finally {
|
|
418
415
|
this.stats.countRequest('home', Date.now() - start);
|
|
419
416
|
}
|
|
@@ -429,11 +426,18 @@ class RegistryModule {
|
|
|
429
426
|
const stats = this.api.getStatistics();
|
|
430
427
|
let html = '';
|
|
431
428
|
|
|
432
|
-
|
|
429
|
+
const data = this.api.getData();
|
|
430
|
+
|
|
431
|
+
if (!data || !data.registries) {
|
|
432
|
+
html += '<div class="alert alert-info">';
|
|
433
|
+
html += '<h4>Registry data not yet available</h4>';
|
|
434
|
+
html += '<p>The initial crawl is in progress. Please refresh in a moment.</p>';
|
|
435
|
+
html += '</div>';
|
|
436
|
+
return html;
|
|
437
|
+
}
|
|
433
438
|
|
|
434
439
|
// Gather all server versions into a flat list
|
|
435
440
|
const serverVersions = [];
|
|
436
|
-
const data = this.api.getData();
|
|
437
441
|
|
|
438
442
|
data.registries.forEach(registry => {
|
|
439
443
|
const authority = registry.authority || '';
|
|
@@ -546,6 +550,8 @@ class RegistryModule {
|
|
|
546
550
|
const data = this.crawler.getData();
|
|
547
551
|
const authCSMap = new Map();
|
|
548
552
|
|
|
553
|
+
if (!data || !data.registries) return [];
|
|
554
|
+
|
|
549
555
|
// Gather all authoritative code systems
|
|
550
556
|
data.registries.forEach(registry => {
|
|
551
557
|
registry.servers.forEach(server => {
|
|
@@ -606,6 +612,8 @@ class RegistryModule {
|
|
|
606
612
|
const data = this.crawler.getData();
|
|
607
613
|
const authVSMap = new Map();
|
|
608
614
|
|
|
615
|
+
if (!data || !data.registries) return [];
|
|
616
|
+
|
|
609
617
|
// Gather all authoritative value sets
|
|
610
618
|
data.registries.forEach(registry => {
|
|
611
619
|
registry.servers.forEach(server => {
|
|
@@ -902,7 +910,7 @@ class RegistryModule {
|
|
|
902
910
|
getStatus() {
|
|
903
911
|
const metadata = this.crawler ? this.crawler.getMetadata() : null;
|
|
904
912
|
const stats = this.api ? this.api.getStatistics() : null;
|
|
905
|
-
|
|
913
|
+
|
|
906
914
|
return {
|
|
907
915
|
enabled: true,
|
|
908
916
|
initialized: this.isInitialized,
|
|
@@ -919,7 +927,7 @@ class RegistryModule {
|
|
|
919
927
|
*/
|
|
920
928
|
async shutdown() {
|
|
921
929
|
this.logger.info('Shutting down Registry module...');
|
|
922
|
-
|
|
930
|
+
|
|
923
931
|
// Stop periodic crawling
|
|
924
932
|
if (this.crawlInterval) {
|
|
925
933
|
clearInterval(this.crawlInterval);
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
|
|
3
|
+
<html xml:lang="en" lang="en">
|
|
4
|
+
<head>
|
|
5
|
+
<title>FHIRsmith: [%title%]</title>
|
|
6
|
+
|
|
7
|
+
<meta charset="utf-8"/>
|
|
8
|
+
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
|
9
|
+
<meta content="http://hl7.org/fhir" name="author"/>
|
|
10
|
+
<meta charset="utf-8" http-equiv="X-UA-Compatible" content="IE=edge" />
|
|
11
|
+
|
|
12
|
+
<link rel="stylesheet" href="/fhir.css"/>
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
<!-- Bootstrap core CSS -->
|
|
16
|
+
<link rel="stylesheet" href="/assets/css/bootstrap.css"/>
|
|
17
|
+
<link rel="stylesheet" href="/assets/css/bootstrap-fhir.css"/>
|
|
18
|
+
|
|
19
|
+
<!-- Project extras -->
|
|
20
|
+
<link rel="stylesheet" href="/assets/css/project.css"/>
|
|
21
|
+
<link rel="stylesheet" href="/assets/css/pygments-manni.css"/>
|
|
22
|
+
|
|
23
|
+
<!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
|
|
24
|
+
<!-- [if lt IE 9]>
|
|
25
|
+
<script src="/assets/js/html5shiv.js"></script>
|
|
26
|
+
<script src="/assets/js/respond.min.js"></script>
|
|
27
|
+
<![endif] -->
|
|
28
|
+
|
|
29
|
+
<!-- Favicons -->
|
|
30
|
+
<link sizes="144x144" rel="apple-touch-icon-precomposed" href="/assets/ico/apple-touch-icon-144-precomposed.png"/>
|
|
31
|
+
<link sizes="114x114" rel="apple-touch-icon-precomposed" href="/assets/ico/apple-touch-icon-114-precomposed.png"/>
|
|
32
|
+
<link sizes="72x72" rel="apple-touch-icon-precomposed" href="/assets/ico/apple-touch-icon-72-precomposed.png"/>
|
|
33
|
+
<link rel="apple-touch-icon-precomposed" href="/assets/ico/apple-touch-icon-57-precomposed.png"/>
|
|
34
|
+
<link rel="shortcut icon" href="/assets/ico/favicon.png"/>
|
|
35
|
+
<script type="text/javascript" src="/assets/js/json2.js"></script>
|
|
36
|
+
<script type="text/javascript" src="/assets/js/statuspage.js"></script>
|
|
37
|
+
<script type="text/javascript" src="/assets/js/jquery.min.js"></script>
|
|
38
|
+
<script type="text/javascript" src="/assets/js/jquery-ui.min.js"></script>
|
|
39
|
+
<link rel="stylesheet" href="/assets/css/jquery.ui.all.css">
|
|
40
|
+
<script type="text/javascript" src="/assets/js/jquery.ui.core.js"></script>
|
|
41
|
+
<script type="text/javascript" src="/assets/js/jquery.ui.widget.js"></script>
|
|
42
|
+
<script type="text/javascript" src="/assets/js/jquery.ui.mouse.js"></script>
|
|
43
|
+
<script type="text/javascript" src="/assets/js/jquery.ui.resizable.js"></script>
|
|
44
|
+
<script type="text/javascript" src="/assets/js/jquery.ui.draggable.js"></script>
|
|
45
|
+
<script type="text/javascript" src="/assets/js/jtip.js"></script>
|
|
46
|
+
<script type="text/javascript" src="/assets/js/jcookie.js"></script>
|
|
47
|
+
<script type="text/javascript" src="/assets/js/fhir-gw.js"></script>
|
|
48
|
+
</head>
|
|
49
|
+
|
|
50
|
+
<body>
|
|
51
|
+
|
|
52
|
+
<!-- /segment-breadcrumb -->
|
|
53
|
+
|
|
54
|
+
<div id="segment-content" class="segment"> <!-- segment-content -->
|
|
55
|
+
<div class="container"> <!-- container -->
|
|
56
|
+
<div class="row">
|
|
57
|
+
<div class="inner-wrapper">
|
|
58
|
+
<div class="col-9">
|
|
59
|
+
|
|
60
|
+
<h2>[%title%] </h2>
|
|
61
|
+
|
|
62
|
+
[%content%]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
</div> <!-- /inner-wrapper -->
|
|
69
|
+
</div> <!-- /row -->
|
|
70
|
+
</div> <!-- /container -->
|
|
71
|
+
</div> <!-- /segment-content -->
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
<!-- JS and analytics only. -->
|
|
78
|
+
<!-- Bootstrap core JavaScript
|
|
79
|
+
================================================== -->
|
|
80
|
+
<!-- Placed at the end of the document so the pages load faster -->
|
|
81
|
+
<!-- <script src="/assets/js/jquery.js"/> -->
|
|
82
|
+
<script src="/assets/js/bootstrap.min.js"/>
|
|
83
|
+
<script src="/assets/js/respond.min.js"/>
|
|
84
|
+
|
|
85
|
+
<script src="/assets/js/fhir.js"/>
|
|
86
|
+
|
|
87
|
+
<!-- Analytics Below
|
|
88
|
+
================================================== -->
|
|
89
|
+
|
|
90
|
+
</body>
|
|
91
|
+
|
|
92
|
+
</html>
|
package/security.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Security Notes for FHIRsmith
|
|
2
|
+
|
|
3
|
+
## Introduction
|
|
4
|
+
|
|
5
|
+
FHIRsmith is a public ready web server. All the modules are considered safe
|
|
6
|
+
to deploy on the public web, but with some caveats that administrators need
|
|
7
|
+
to pay attention to.
|
|
8
|
+
|
|
9
|
+
## Supported Versions
|
|
10
|
+
|
|
11
|
+
At this time, only the latest version is supported for security updates.
|
|
12
|
+
|
|
13
|
+
## Reporting Security Issues
|
|
14
|
+
|
|
15
|
+
Use the standard GitHub security reporting framework.
|
|
16
|
+
|
|
17
|
+
## Rate Limiting
|
|
18
|
+
|
|
19
|
+
Some modules make extensive use of the file system and/or SQLite databases.
|
|
20
|
+
The server does not itself have any rate limiting arrangements; instead, it
|
|
21
|
+
is expected that the server will be deployed behind NGINX (or similar), and
|
|
22
|
+
that NGINX will be configured to provide rate limiting as appropriate.
|
|
23
|
+
|
|
24
|
+
A typical NGINX configuration would be:
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
limit_req zone=general burst=6000 delay=20;
|
|
28
|
+
limit_req_status 429;
|
|
29
|
+
limit_conn perip 50;
|
|
30
|
+
limit_conn perserver 500;
|
|
31
|
+
```
|
|
32
|
+
|