fhirsmith 0.8.5 → 0.8.6

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/library/logger.js CHANGED
@@ -1,8 +1,25 @@
1
- const winston = require('winston');
2
- require('winston-daily-rotate-file');
3
1
  const fs = require('fs');
2
+ const path = require('path');
4
3
  const folders = require('./folder-setup');
5
4
 
5
+ // ---------------------------------------------------------------------------
6
+ // Buffered, daily-rotating logger
7
+ // - Writes are batched and flushed every FLUSH_INTERVAL ms or FLUSH_SIZE lines
8
+ // - this is intended to be highly efficient
9
+ // ---------------------------------------------------------------------------
10
+
11
+ const DEFAULTS = {
12
+ level: 'info', // error, warn, info, debug, verbose
13
+ console: true, // write to stdout/stderr
14
+ consoleErrors: false, // include error/warn on console (when running as service, these go to journal)
15
+ maxFiles: 14, // number of daily log files to keep
16
+ maxSize: 0, // max bytes per file (0 = unlimited)
17
+ flushInterval: 2000, // ms between flushes
18
+ flushSize: 200, // flush when buffer reaches this many lines
19
+ };
20
+
21
+ const LEVELS = { error: 0, warn: 1, info: 2, debug: 3, verbose: 4 };
22
+
6
23
  class Logger {
7
24
  static _instance = null;
8
25
 
@@ -13,260 +30,278 @@ class Logger {
13
30
  return Logger._instance;
14
31
  }
15
32
 
33
+ // options = config.logging section from config.json (all fields optional)
34
+ //
35
+ // Example config.json:
36
+ // {
37
+ // "logging": {
38
+ // "level": "info",
39
+ // "console": false,
40
+ // "consoleErrors": false,
41
+ // "maxFiles": 14,
42
+ // "maxSize": "50m",
43
+ // "flushInterval": 2000,
44
+ // "flushSize": 200
45
+ // }
46
+ // }
47
+ //
16
48
  constructor(options = {}) {
17
- this.options = {
18
- level: options.level || 'info',
19
- logDir: options.logDir || folders.logsDir(),
20
- console: options.console !== undefined ? options.console : true,
21
- consoleErrors: options.consoleErrors !== undefined ? options.consoleErrors : false,
22
- telnetErrors: options.telnetErrors !== undefined ? options.telnetErrors : false,
23
- file: {
24
- filename: options.file?.filename || 'server-%DATE%.log',
25
- datePattern: options.file?.datePattern || 'YYYY-MM-DD',
26
- maxSize: options.file?.maxSize || '20m',
27
- maxFiles: options.file?.maxFiles || 14
28
- }
29
- };
49
+ this.level = options.level || DEFAULTS.level;
50
+ this.logDir = options.logDir || folders.logsDir();
51
+ this.maxFiles = options.maxFiles ?? DEFAULTS.maxFiles;
52
+ this.maxSize = Logger._parseSize(options.maxSize) || DEFAULTS.maxSize;
53
+ this.showConsole = options.console ?? DEFAULTS.console;
54
+ this.consoleErrors = options.consoleErrors ?? DEFAULTS.consoleErrors;
55
+
56
+ const flushInterval = options.flushInterval ?? DEFAULTS.flushInterval;
57
+ this._flushSize = options.flushSize ?? DEFAULTS.flushSize;
30
58
 
31
59
  // Ensure log directory exists
32
- if (!fs.existsSync(this.options.logDir)) {
33
- fs.mkdirSync(this.options.logDir, { recursive: true });
60
+ if (!fs.existsSync(this.logDir)) {
61
+ fs.mkdirSync(this.logDir, { recursive: true });
34
62
  }
35
63
 
36
- // Telnet clients storage
37
- this.telnetClients = new Set();
64
+ // Buffer
65
+ this._buffer = [];
66
+ this._currentDate = null;
67
+ this._fd = null;
68
+ this._currentFileSize = 0;
38
69
 
39
- this._createLogger();
70
+ // Periodic flush
71
+ this._flushTimer = setInterval(() => this._flush(), flushInterval);
72
+ if (this._flushTimer.unref) this._flushTimer.unref(); // don't keep process alive
40
73
 
41
- // Log logger initialization
42
- this.info('Logger initialized @ ' + this.options.logDir, {
43
- level: this.options.level,
44
- logDir: this.options.logDir
45
- });
74
+ // Flush on exit
75
+ process.on('exit', () => this._flushSync());
76
+
77
+ this.info('Logger initialized @ ' + this.logDir, {});
46
78
  }
47
79
 
48
- _createLogger() {
49
- // Define formats for file output (with full metadata including stack traces)
50
- const fileFormats = [
51
- winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss.SSS' }),
52
- winston.format.errors({ stack: true }),
53
- winston.format.splat(),
54
- winston.format.json()
55
- ];
56
-
57
- // Create transports
58
- const transports = [];
59
-
60
- // Add file transport with rotation (includes ALL levels with full metadata)
61
- const fileTransport = new winston.transports.DailyRotateFile({
62
- dirname: this.options.logDir,
63
- filename: this.options.file.filename,
64
- datePattern: this.options.file.datePattern,
65
- maxSize: this.options.file.maxSize,
66
- maxFiles: this.options.file.maxFiles,
67
- level: this.options.level,
68
- format: winston.format.combine(...fileFormats)
69
- });
70
- transports.push(fileTransport);
71
-
72
- // Add console transport if enabled
73
- if (this.options.console) {
74
- const consoleFormat = winston.format.combine(
75
- winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss.SSS' }),
76
- winston.format.errors({ stack: true }),
77
- winston.format.colorize({ all: true }),
78
- winston.format.printf(info => {
79
- const stack = info.stack ? `\n${info.stack}` : '';
80
- return `${info.timestamp} ${info.level.padEnd(7)} ${info.message}${stack}`;
81
- })
82
- );
83
-
84
- const consoleTransport = new winston.transports.Console({
85
- level: this.options.level,
86
- format: consoleFormat
87
- });
88
-
89
- transports.push(consoleTransport);
80
+ // Parse human-readable size strings: "20m" -> bytes, "1g" -> bytes
81
+ static _parseSize(value) {
82
+ if (!value) return 0;
83
+ if (typeof value === 'number') return value;
84
+ const m = String(value).match(/^(\d+(?:\.\d+)?)\s*([kmg])?b?$/i);
85
+ if (!m) return 0;
86
+ const num = parseFloat(m[1]);
87
+ switch ((m[2] || '').toLowerCase()) {
88
+ case 'k': return num * 1024;
89
+ case 'm': return num * 1024 * 1024;
90
+ case 'g': return num * 1024 * 1024 * 1024;
91
+ default: return num;
90
92
  }
93
+ }
91
94
 
92
- // Create the winston logger
93
- this.logger = winston.createLogger({
94
- level: this.options.level,
95
- transports,
96
- exitOnError: false
97
- });
95
+ // Compatibility: server.js home page reads Logger.getInstance().options.file.maxFiles etc.
96
+ get options() {
97
+ return {
98
+ level: this.level,
99
+ file: {
100
+ maxFiles: this.maxFiles,
101
+ maxSize: this.maxSize > 0 ? `${Math.round(this.maxSize / 1024 / 1024)}m` : 'unlimited',
102
+ }
103
+ };
98
104
  }
99
105
 
100
- // Telnet client management
101
- addTelnetClient(socket) {
102
- this.telnetClients.add(socket);
106
+ // --- formatting (inline, no libraries) ---
107
+
108
+ _timestamp() {
109
+ const d = new Date();
110
+ const Y = d.getFullYear();
111
+ const M = String(d.getMonth() + 1).padStart(2, '0');
112
+ const D = String(d.getDate()).padStart(2, '0');
113
+ const h = String(d.getHours()).padStart(2, '0');
114
+ const m = String(d.getMinutes()).padStart(2, '0');
115
+ const s = String(d.getSeconds()).padStart(2, '0');
116
+ const ms = String(d.getMilliseconds()).padStart(3, '0');
117
+ return `${Y}-${M}-${D} ${h}:${m}:${s}.${ms}`;
103
118
  }
104
119
 
105
- removeTelnetClient(socket) {
106
- this.telnetClients.delete(socket);
120
+ _dateTag() {
121
+ const d = new Date();
122
+ const Y = d.getFullYear();
123
+ const M = String(d.getMonth() + 1).padStart(2, '0');
124
+ const D = String(d.getDate()).padStart(2, '0');
125
+ return `${Y}-${M}-${D}`;
107
126
  }
108
127
 
109
- _sendToTelnet(level, message, stack, options) {
110
- // Check if we should send errors/warnings to telnet
111
- if (!options.telnetErrors && (level === 'error' || level === 'warn')) {
112
- return;
113
- }
128
+ _formatLine(level, message, stack) {
129
+ const ts = this._timestamp();
130
+ const lv = level.padEnd(7);
131
+ let line = `${ts} ${lv} ${message}\n`;
132
+ if (stack) line += stack + '\n';
133
+ return line;
134
+ }
135
+
136
+ // --- file management ---
114
137
 
115
- const timestamp = new Date().toISOString().replace('T', ' ').substring(0, 23);
116
- let line = `${timestamp} ${level.padEnd(7)} ${message}\n`;
117
- if (stack) {
118
- line += stack + '\n';
138
+ _openFile(dateTag) {
139
+ // Check if we need to rotate due to size
140
+ if (this._fd !== null && this._currentDate === dateTag) {
141
+ if (this.maxSize <= 0 || this._currentFileSize < this.maxSize) return;
142
+ // Size limit exceeded — close and fall through to open a new file
143
+ try { fs.closeSync(this._fd); } catch (_) { /* intentional */ }
144
+ this._fd = null;
119
145
  }
146
+ // Close previous (date changed)
147
+ if (this._fd !== null) {
148
+ try { fs.closeSync(this._fd); } catch (_) { /* intentional */ }
149
+ }
150
+ const filename = `server-${dateTag}.log`;
151
+ const filePath = path.join(this.logDir, filename);
152
+ this._fd = fs.openSync(filePath, 'a');
153
+ try { this._currentFileSize = fs.fstatSync(this._fd).size; } catch (_) { this._currentFileSize = 0; }
154
+ this._currentDate = dateTag;
155
+
156
+ // Maintain a stable symlink so `tail -f server.log` always tracks the current file
157
+ const linkPath = path.join(this.logDir, 'server.log');
158
+ try { fs.unlinkSync(linkPath); } catch (_) { /* intentional */ }
159
+ try { fs.symlinkSync(filename, linkPath); } catch (_) { /* intentional */ }
160
+
161
+ this._purgeOldFiles();
162
+ }
120
163
 
121
- for (const client of this.telnetClients) {
122
- try {
123
- client.write(line);
124
- } catch (e) {
125
- // Client disconnected, remove it
126
- this.telnetClients.delete(client);
164
+ _purgeOldFiles() {
165
+ try {
166
+ const files = fs.readdirSync(this.logDir)
167
+ .filter(f => f.startsWith('server-') && f.endsWith('.log'))
168
+ .sort();
169
+ while (files.length > this.maxFiles) {
170
+ const old = files.shift();
171
+ fs.unlinkSync(path.join(this.logDir, old));
127
172
  }
128
- }
173
+ } catch (_) { /* intentional */ }
129
174
  }
130
175
 
131
- _shouldLogToConsole(level, options) {
132
- if (level === 'error' || level === 'warn') {
133
- return options.consoleErrors;
176
+ // --- buffer + flush ---
177
+
178
+ _enqueue(line) {
179
+ this._buffer.push(line);
180
+ if (this._buffer.length >= this._flushSize) {
181
+ this._flush();
134
182
  }
135
- return true;
183
+ }
184
+
185
+ _flush() {
186
+ if (this._buffer.length === 0) return;
187
+ const dateTag = this._dateTag();
188
+ this._openFile(dateTag);
189
+ const chunk = this._buffer.join('');
190
+ this._buffer.length = 0;
191
+ // Async write — fire and forget; OS will buffer anyway
192
+ const buf = Buffer.from(chunk);
193
+ this._currentFileSize += buf.length;
194
+ fs.write(this._fd, buf, 0, buf.length, null, (err) => {
195
+ if (err) {
196
+ // If the fd went bad (e.g. date rolled), reopen and retry once
197
+ try {
198
+ this._currentDate = null;
199
+ this._openFile(this._dateTag());
200
+ fs.writeSync(this._fd, buf, 0, buf.length);
201
+ } catch (_) { /* intentional */ }
202
+ }
203
+ });
204
+ }
205
+
206
+ _flushSync() {
207
+ if (this._buffer.length === 0) return;
208
+ const dateTag = this._dateTag();
209
+ this._openFile(dateTag);
210
+ const chunk = this._buffer.join('');
211
+ this._buffer.length = 0;
212
+ try { fs.writeSync(this._fd, chunk); } catch (_) { /* intentional */ }
213
+ }
214
+
215
+ // --- core log ---
216
+
217
+ _shouldLog(level) {
218
+ return (LEVELS[level] ?? 99) <= (LEVELS[this.level] ?? 2);
136
219
  }
137
220
 
138
221
  _log(level, messageOrError, meta, options) {
222
+ if (!this._shouldLog(level)) return;
223
+
139
224
  let message;
140
225
  let stack;
141
226
 
142
- // Check if we should skip console for errors/warnings
143
- const skipConsole = !this._shouldLogToConsole(level, options);
144
-
145
- // Handle Error objects
146
227
  if (messageOrError instanceof Error) {
147
228
  message = messageOrError.message;
148
229
  stack = messageOrError.stack;
149
- if (skipConsole) {
150
- // Log only to file transport
151
- this.logger.transports
152
- .filter(t => !(t instanceof winston.transports.Console))
153
- .forEach(t => t.log({ level, message, stack, ...meta }));
154
- } else {
155
- this.logger[level](message, {stack, ...meta});
156
- }
157
230
  } else {
158
231
  message = String(messageOrError);
159
232
  stack = meta?.stack;
160
- if (skipConsole) {
161
- this.logger.transports
162
- .filter(t => !(t instanceof winston.transports.Console))
163
- .forEach(t => t.log({ level, message, ...meta }));
164
- } else {
165
- this.logger[level](message, meta);
166
- }
167
233
  }
168
234
 
169
- this._sendToTelnet(level, message, stack, options);
170
- }
171
-
172
- error(message, meta = {}) {
173
- this._log('error', message, meta, this.options);
174
- }
175
-
176
- warn(message, meta = {}) {
177
- this._log('warn', message, meta, this.options);
178
- }
179
-
180
- info(message, meta = {}) {
181
- this._log('info', message, meta, this.options);
235
+ const line = this._formatLine(level, message, stack);
236
+
237
+ // Buffer for file
238
+ this._enqueue(line);
239
+
240
+ // Console
241
+ if (this.showConsole) {
242
+ const isErrWarn = level === 'error' || level === 'warn';
243
+ const consoleErrors = options?.consoleErrors ?? this.consoleErrors;
244
+ if (!isErrWarn || consoleErrors) {
245
+ if (isErrWarn) {
246
+ process.stderr.write(line);
247
+ } else {
248
+ process.stdout.write(line);
249
+ }
250
+ }
251
+ }
182
252
  }
183
253
 
184
- debug(message, meta = {}) {
185
- this._log('debug', message, meta, this.options);
186
- }
254
+ // --- public API (same as before) ---
187
255
 
188
- verbose(message, meta = {}) {
189
- this._log('verbose', message, meta, this.options);
190
- }
256
+ error(message, meta = {}) { this._log('error', message, meta, this); }
257
+ warn(message, meta = {}) { this._log('warn', message, meta, this); }
258
+ info(message, meta = {}) { this._log('info', message, meta, this); }
259
+ debug(message, meta = {}) { this._log('debug', message, meta, this); }
260
+ verbose(message, meta = {}) { this._log('verbose', message, meta, this); }
191
261
 
192
- log(level, message, meta = {}) {
193
- this._log(level, message, meta, this.options);
194
- }
262
+ log(level, message, meta = {}) { this._log(level, message, meta, this); }
195
263
 
196
264
  child(defaultMeta = {}) {
197
265
  const self = this;
198
266
 
199
- // Build module-specific options
200
267
  const childOptions = {
201
- consoleErrors: defaultMeta.consoleErrors ?? self.options.consoleErrors,
202
- telnetErrors: defaultMeta.telnetErrors ?? self.options.telnetErrors
268
+ consoleErrors: defaultMeta.consoleErrors ?? self.consoleErrors,
203
269
  };
204
270
 
205
- // Remove our custom options from defaultMeta so they don't pollute log output
206
- const cleanMeta = { ...defaultMeta };
207
- delete cleanMeta.consoleErrors;
208
- delete cleanMeta.telnetErrors;
209
-
210
- if (cleanMeta.module) {
211
- const modulePrefix = `{${cleanMeta.module}}`;
212
-
213
- return {
214
- error: (messageOrError, meta = {}) => {
215
- if (messageOrError instanceof Error) {
216
- const prefixedError = new Error(`${modulePrefix}: ${messageOrError.message}`);
217
- prefixedError.stack = messageOrError.stack;
218
- self._log('error', prefixedError, meta, childOptions);
219
- } else {
220
- self._log('error', `${modulePrefix}: ${messageOrError}`, meta, childOptions);
221
- }
222
- },
223
- warn: (messageOrError, meta = {}) => {
224
- if (messageOrError instanceof Error) {
225
- const prefixedError = new Error(`${modulePrefix}: ${messageOrError.message}`);
226
- prefixedError.stack = messageOrError.stack;
227
- self._log('warn', prefixedError, meta, childOptions);
228
- } else {
229
- self._log('warn', `${modulePrefix}: ${messageOrError}`, meta, childOptions);
230
- }
231
- },
232
- info: (message, meta = {}) => self._log('info', `${modulePrefix}: ${message}`, meta, childOptions),
233
- debug: (message, meta = {}) => self._log('debug', `${modulePrefix}: ${message}`, meta, childOptions),
234
- verbose: (message, meta = {}) => self._log('verbose', `${modulePrefix}: ${message}`, meta, childOptions),
235
- log: (level, message, meta = {}) => self._log(level, `${modulePrefix}: ${message}`, meta, childOptions)
236
- };
237
- }
271
+ const modulePrefix = defaultMeta.module ? `{${defaultMeta.module}}` : null;
238
272
 
239
- // For other metadata without module prefix
240
- const childLogger = {
241
- error: (messageOrError, meta = {}) => self._log('error', messageOrError, { ...cleanMeta, ...meta }, childOptions),
242
- warn: (messageOrError, meta = {}) => self._log('warn', messageOrError, { ...cleanMeta, ...meta }, childOptions),
243
- info: (message, meta = {}) => self._log('info', message, { ...cleanMeta, ...meta }, childOptions),
244
- debug: (message, meta = {}) => self._log('debug', message, { ...cleanMeta, ...meta }, childOptions),
245
- verbose: (message, meta = {}) => self._log('verbose', message, { ...cleanMeta, ...meta }, childOptions),
246
- log: (level, message, meta = {}) => self._log(level, message, { ...cleanMeta, ...meta }, childOptions)
273
+ const wrap = (level) => (messageOrError, meta = {}) => {
274
+ if (messageOrError instanceof Error) {
275
+ const prefixed = modulePrefix
276
+ ? Object.assign(new Error(`${modulePrefix}: ${messageOrError.message}`), { stack: messageOrError.stack })
277
+ : messageOrError;
278
+ self._log(level, prefixed, meta, childOptions);
279
+ } else {
280
+ const msg = modulePrefix ? `${modulePrefix}: ${messageOrError}` : String(messageOrError);
281
+ self._log(level, msg, meta, childOptions);
282
+ }
247
283
  };
248
284
 
249
- return childLogger;
285
+ return {
286
+ error: wrap('error'),
287
+ warn: wrap('warn'),
288
+ info: wrap('info'),
289
+ debug: wrap('debug'),
290
+ verbose: wrap('verbose'),
291
+ log: (level, message, meta = {}) => wrap(level)(message, meta)
292
+ };
250
293
  }
251
294
 
252
295
  setLevel(level) {
253
- this.options.level = level;
254
- this.logger.transports.forEach(transport => {
255
- transport.level = level;
256
- });
296
+ this.level = level;
257
297
  this.info(`Log level changed to ${level}`);
258
298
  }
259
299
 
260
300
  setConsoleErrors(enabled) {
261
- this.options.consoleErrors = enabled;
301
+ this.consoleErrors = enabled;
262
302
  this.info(`Console errors ${enabled ? 'enabled' : 'disabled'}`);
263
303
  }
264
304
 
265
- setTelnetErrors(enabled) {
266
- this.options.telnetErrors = enabled;
267
- this.info(`Telnet errors ${enabled ? 'enabled' : 'disabled'}`);
268
- }
269
-
270
305
  stream() {
271
306
  return {
272
307
  write: (message) => {
@@ -274,6 +309,11 @@ class Logger {
274
309
  }
275
310
  };
276
311
  }
312
+
313
+ // Force an immediate flush (e.g. before graceful shutdown)
314
+ flush() {
315
+ this._flushSync();
316
+ }
277
317
  }
278
318
 
279
319
  module.exports = Logger;
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "fhirsmith",
3
- "version": "0.8.5",
3
+ "version": "0.8.6",
4
+ "txVersion": "1.9.1",
4
5
  "description": "A Node.js server that provides a collection of tools to serve the FHIR ecosystem",
5
6
  "main": "server.js",
6
7
  "engines": {
@@ -92,7 +92,9 @@
92
92
  <p>
93
93
  <a href="http://www.hl7.org/fhir" style="color: gold" title="Fast Healthcare Interoperability Resources - Home Page"><img border="0" src="/icon-fhir-16.png" style="vertical-align: text-bottom"/> <b>FHIR</b></a> &copy; HL7.org 2011+. &nbsp;|&nbsp;
94
94
  <a href="https://github.com/HealthIntersections/FHIRsmith/blob/main/README.md" style="color: gold"><img border="0" src="/FHIRsmith16.png" style="vertical-align: text-bottom"/> FHIRsmith</a> [%ver%] &copy; HealthIntersections.com.au 2023+ &nbsp;|&nbsp;
95
- Package Registry last updated as of [%crawler-date%] &nbsp;|&nbsp; [%total-packages%] packages &nbsp;|&nbsp; ([%ms%] ms)
95
+ Package Registry last updated as of [%crawler-date%] &nbsp;|&nbsp; [%total-packages%] packages &nbsp;|
96
+ &nbsp; ([%ms%] ms)
97
+ [%sponsorMessage%]
96
98
  </p>
97
99
  </div> <!-- /inner-wrapper -->
98
100
  </div> <!-- /container -->
@@ -123,6 +123,7 @@
123
123
  <a href="http://www.hl7.org/fhir" style="color: gold" title="Fast Healthcare Interoperability Resources - Home Page"><img border="0" src="/icon-fhir-16.png" style="vertical-align: text-bottom"/> <b>FHIR</b></a> &copy; HL7.org 2011+. &nbsp;|&nbsp;
124
124
  <a href="https://github.com/HealthIntersections/FHIRsmith/blob/main/README.md" style="color: gold"><img border="0" src="/FHIRsmith16.png" style="vertical-align: text-bottom"/> FHIRsmith</a> [%ver%] &copy; HealthIntersections.com.au 2023+ &nbsp;|&nbsp;
125
125
  ([%ms%] ms)
126
+ [%sponsorMessage%]
126
127
  </p>
127
128
  </div> <!-- /inner-wrapper -->
128
129
  </div> <!-- /container -->
@@ -92,7 +92,9 @@
92
92
  <p>
93
93
  <a href="http://www.hl7.org/fhir" style="color: gold" title="Fast Healthcare Interoperability Resources - Home Page"><img border="0" src="/icon-fhir-16.png" style="vertical-align: text-bottom"/> <b>FHIR</b></a> &copy; HL7.org 2011+. &nbsp;|&nbsp;
94
94
  <a href="https://github.com/HealthIntersections/FHIRsmith/blob/main/README.md" style="color: gold"><img border="0" src="/FHIRsmith16.png" style="vertical-align: text-bottom"/> FHIRsmith</a> [%ver%] &copy; HealthIntersections.com.au 2023+ &nbsp;|&nbsp;
95
- Terminology Registry last updated as of [%crawler-date%] &nbsp;|&nbsp; [%total-packages%] packages &nbsp;|&nbsp; ([%ms%] ms)
95
+ Terminology Registry last updated as of [%crawler-date%] &nbsp;|&nbsp; [%total-packages%] packages &nbsp;|
96
+ &nbsp; ([%ms%] ms)
97
+ [%sponsorMessage%]
96
98
  </p>
97
99
  </div> <!-- /inner-wrapper -->
98
100
  </div> <!-- /container -->
@@ -90,8 +90,9 @@
90
90
  <div class="inner-wrapper">
91
91
  <p>
92
92
  <a href="http://www.hl7.org/fhir" style="color: gold" title="Fast Healthcare Interoperability Resources - Home Page"><img border="0" src="/icon-fhir-16.png" style="vertical-align: text-bottom"/> <b>FHIR</b></a> &copy; HL7.org 2011+. &nbsp;|&nbsp;
93
- <a href="https://github.com/HealthIntersections/FHIRsmith/blob/main/README.md" style="color: gold"><img border="0" src="/FHIRsmith16.png" style="vertical-align: text-bottom"/> FHIRsmith</a> [%ver%] &copy; HealthIntersections.com.au 2023+ &nbsp;|&nbsp; ([%ms%] ms)
94
- &nbsp;
93
+ <a href="https://github.com/HealthIntersections/FHIRsmith/blob/main/README.md" style="color: gold"><img border="0" src="/FHIRsmith16.png" style="vertical-align: text-bottom"/> FHIRsmith</a> [%ver%] &copy; HealthIntersections.com.au 2023+ &nbsp;|
94
+ &nbsp; ([%ms%] ms)
95
+ [%sponsorMessage%]
95
96
  </p>
96
97
  </div> <!-- /inner-wrapper -->
97
98
  </div> <!-- /container -->