diff --git a/server.js b/server.js
index 997bc9c..14cb176 100644
--- a/server.js
+++ b/server.js
@@ -10,6 +10,7 @@ const express = require('express');
// const cors = require('cors');
const path = require('path');
const fs = require('fs');
+const os = require('os');
const folders = require('./library/folder-setup'); // <-- ADD: load early
const { statSync, readdirSync } = require('fs');
const escape = require('escape-html');
@@ -27,6 +28,17 @@ try {
const Logger = require('./library/logger');
const serverLog = Logger.getInstance().child({ module: 'server' });
+const packageJson = require('./package.json');
+
+// Startup banner
+const totalMemGB = (os.totalmem() / 1024 / 1024 / 1024).toFixed(1);
+const freeMemGB = (os.freemem() / 1024 / 1024 / 1024).toFixed(1);
+serverLog.info(`========================================`);
+serverLog.info(`FHIRsmith v${packageJson.version} starting (PID ${process.pid})`);
+serverLog.info(`Node.js ${process.version} on ${os.type()} ${os.release()} (${os.arch()})`);
+serverLog.info(`Memory: ${freeMemGB} GB free / ${totalMemGB} GB total`);
+serverLog.info(`Data directory: ${folders.dataDir()}`);
+serverLog.info(`========================================`);
const activeModules = config.modules ? Object.keys(config.modules)
.filter(mod => config.modules[mod].enabled)
@@ -43,7 +55,6 @@ const PublisherModule = require('./publisher/publisher.js');
const TokenModule = require('./token/token.js');
const NpmProjectorModule = require('./npmprojector/npmprojector.js');
const TXModule = require('./tx/tx.js');
-const packageJson = require('./package.json');
const htmlServer = require('./library/html-server');
const ServerStats = require("./stats");
@@ -81,6 +92,7 @@ async function initializeModules() {
// Initialize SHL module
if (config.modules?.shl?.enabled) {
try {
+ serverLog.info('Initializing module: shl...');
modules.shl = new SHLModule(stats);
await modules.shl.initialize(config.modules.shl);
app.use('/shl', modules.shl.router);
@@ -93,6 +105,7 @@ async function initializeModules() {
// Initialize VCL module
if (config.modules?.vcl?.enabled) {
try {
+ serverLog.info('Initializing module: vcl...');
modules.vcl = new VCLModule(stats);
await modules.vcl.initialize(config.modules.vcl);
app.use('/VCL', modules.vcl.router);
@@ -101,11 +114,12 @@ async function initializeModules() {
throw error;
}
}
-
+
// Initialize XIG module
if (config.modules?.xig?.enabled) {
try {
- await xigModule.initializeXigModule(stats);
+ serverLog.info('Initializing module: xig...');
+ await xigModule.initializeXigModule(stats, config.modules.xig);
app.use('/xig', xigModule.router);
modules.xig = xigModule;
} catch (error) {
@@ -117,6 +131,7 @@ async function initializeModules() {
// Initialize Packages module
if (config.modules?.packages?.enabled) {
try {
+ serverLog.info('Initializing module: packages...');
modules.packages = new PackagesModule(stats);
await modules.packages.initialize(config.modules.packages);
app.use('/packages', modules.packages.router);
@@ -130,6 +145,7 @@ async function initializeModules() {
// Initialize Registry module
if (config.modules?.registry?.enabled) {
try {
+ serverLog.info('Initializing module: registry...');
modules.registry = new RegistryModule(stats);
await modules.registry.initialize(config.modules.registry);
app.use('/tx-reg', modules.registry.router);
@@ -142,6 +158,7 @@ async function initializeModules() {
// Initialize Publisher module
if (config.modules?.publisher?.enabled) {
try {
+ serverLog.info('Initializing module: publisher...');
modules.publisher = new PublisherModule(stats);
await modules.publisher.initialize(config.modules.publisher);
app.use('/publisher', modules.publisher.router);
@@ -154,6 +171,7 @@ async function initializeModules() {
// Initialize Token module
if (config.modules?.token?.enabled) {
try {
+ serverLog.info('Initializing module: token...');
modules.token = new TokenModule(stats);
await modules.token.initialize(config.modules.token);
app.use('/token', modules.token.router);
@@ -166,6 +184,7 @@ async function initializeModules() {
// Initialize NpmProjector module
if (config.modules?.npmprojector?.enabled) {
try {
+ serverLog.info('Initializing module: npmprojector...');
modules.npmprojector = new NpmProjectorModule(stats);
await modules.npmprojector.initialize(config.modules.npmprojector);
const basePath = NpmProjectorModule.getBasePath(config.modules.npmprojector);
@@ -181,6 +200,7 @@ async function initializeModules() {
// because it supports multiple endpoints at different paths
if (config.modules?.tx?.enabled) {
try {
+ serverLog.info('Initializing module: tx...');
modules.tx = new TXModule(stats);
await modules.tx.initialize(config.modules.tx, app);
} catch (error) {
@@ -365,6 +385,13 @@ async function buildRootPageContent() {
// eslint-disable-next-line no-unused-vars
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection:', reason);
+ serverLog.error('Unhandled Rejection:', reason);
+});
+
+process.on('uncaughtException', (error) => {
+ console.error('FATAL - Uncaught Exception:', error);
+ serverLog.error('FATAL - Uncaught Exception:', error);
+ process.exitCode = 1;
});
app.get('/', async (req, res) => {
@@ -382,7 +409,7 @@ app.get('/', async (req, res) => {
}
const content = await buildRootPageContent();
-
+
// Build basic stats for root page
const stats = {
version: packageJson.version,
@@ -431,7 +458,7 @@ app.get('/', async (req, res) => {
Object.keys(enabledModules)
.filter(m => m !== 'tx')
.map(m => [
- m,
+ m,
m === 'vcl' ? '/VCL' : `/${m}`
])
),
@@ -542,8 +569,10 @@ async function startServer() {
modules.packages.startInitialCrawler();
}
} catch (error) {
- serverLog.error('Failed to start server:', error);
- process.exit(1);
+ console.error('FATAL - Failed to start server:', error);
+ serverLog.error('FATAL - Failed to start server:', error);
+ // Give the logger a moment to flush before exiting
+ setTimeout(() => process.exit(1), 500);
}
}
diff --git a/xig/xig.js b/xig/xig.js
index 60f77ce..4805b4f 100644
--- a/xig/xig.js
+++ b/xig/xig.js
@@ -2213,22 +2213,22 @@ router.get('/:packagePid/:resourceType/:resourceId', async (req, res) => {
const start = Date.now();
try {
- const { packagePid, resourceType, resourceId } = req.params;
+ const { packagePid, resourceType, resourceId } = req.params;
- // Check if this looks like a package/resource pattern
- // Package PIDs typically contain dots and pipes: hl7.fhir.uv.extensions|current
- // Resource types are FHIR resource names: StructureDefinition, ValueSet, etc.
+ // Check if this looks like a package/resource pattern
+ // Package PIDs typically contain dots and pipes: hl7.fhir.uv.extensions|current
+ // Resource types are FHIR resource names: StructureDefinition, ValueSet, etc.
- const isPackagePidFormat = packagePid.includes('.') || packagePid.includes('|');
- const isFhirResourceType = /^[A-Z][a-zA-Z]+$/.test(resourceType);
+ const isPackagePidFormat = packagePid.includes('.') || packagePid.includes('|');
+ const isFhirResourceType = /^[A-Z][a-zA-Z]+$/.test(resourceType);
- if (isPackagePidFormat && isFhirResourceType) {
- // This looks like a legacy resource URL, redirect to the proper format
- res.redirect(301, `/xig/resource/${packagePid}/${resourceType}/${resourceId}`);
- } else {
- // Not a resource URL pattern, return 404
- res.status(404).send('Not Found');
- }
+ if (isPackagePidFormat && isFhirResourceType) {
+ // This looks like a legacy resource URL, redirect to the proper format
+ res.redirect(301, `/xig/resource/${packagePid}/${resourceType}/${resourceId}`);
+ } else {
+ // Not a resource URL pattern, return 404
+ res.status(404).send('Not Found');
+ }
} finally {
this.stats.countRequest(':id', Date.now() - start);
}
@@ -2333,74 +2333,74 @@ router.get('/stats', async (req, res) => {
const start = Date.now();
try {
- const startTime = Date.now(); // Add this at the very beginning
-
- try {
+ const startTime = Date.now(); // Add this at the very beginning
- const [dbInfo, tableCounts] = await Promise.all([
- getDatabaseInfo(),
- getDatabaseTableCounts()
- ]);
+ try {
- const statsData = {
- cache: getCacheStats(),
- database: dbInfo,
- databaseAge: getDatabaseAgeInfo(),
- tableCounts: tableCounts,
- requests: getRequestStats()
- };
+ const [dbInfo, tableCounts] = await Promise.all([
+ getDatabaseInfo(),
+ getDatabaseTableCounts()
+ ]);
+
+ const statsData = {
+ cache: getCacheStats(),
+ database: dbInfo,
+ databaseAge: getDatabaseAgeInfo(),
+ tableCounts: tableCounts,
+ requests: getRequestStats()
+ };
- const content = buildStatsTable(statsData);
-
- let introContent = '';
- const lastAttempt = getLastUpdateAttempt();
-
- if (statsData.databaseAge.daysOld !== null && statsData.databaseAge.daysOld > 1) {
- introContent += `
`;
- introContent += `
⚠ Database is ${statsData.databaseAge.daysOld} days old. `;
- introContent += `Automatic updates are scheduled daily at 2 AM. `;
- if (lastAttempt) {
- if (lastAttempt.status === 'failed') {
- introContent += `
Last update attempt failed at ${new Date(lastAttempt.timestamp).toLocaleString()}: `;
- introContent += `${escape(lastAttempt.error || 'Unknown error')}`;
- if (lastAttempt.downloadMeta && lastAttempt.downloadMeta.httpStatus) {
- introContent += ` (HTTP ${lastAttempt.downloadMeta.httpStatus})`;
+ const content = buildStatsTable(statsData);
+
+ let introContent = '';
+ const lastAttempt = getLastUpdateAttempt();
+
+ if (statsData.databaseAge.daysOld !== null && statsData.databaseAge.daysOld > 1) {
+ introContent += `
`;
+ introContent += `⚠ Database is ${statsData.databaseAge.daysOld} days old. `;
+ introContent += `Automatic updates are scheduled daily at 2 AM. `;
+ if (lastAttempt) {
+ if (lastAttempt.status === 'failed') {
+ introContent += `
Last update attempt failed at ${new Date(lastAttempt.timestamp).toLocaleString()}: `;
+ introContent += `${escape(lastAttempt.error || 'Unknown error')}`;
+ if (lastAttempt.downloadMeta && lastAttempt.downloadMeta.httpStatus) {
+ introContent += ` (HTTP ${lastAttempt.downloadMeta.httpStatus})`;
+ }
+ } else if (lastAttempt.status === 'success') {
+ introContent += `
Last successful update: ${new Date(lastAttempt.timestamp).toLocaleString()} `;
+ introContent += `(file age based on filesystem mtime)`;
}
- } else if (lastAttempt.status === 'success') {
- introContent += `
Last successful update: ${new Date(lastAttempt.timestamp).toLocaleString()} `;
- introContent += `(file age based on filesystem mtime)`;
+ } else {
+ introContent += `
No update attempts recorded since server started.`;
}
- } else {
- introContent += `
No update attempts recorded since server started.`;
+ introContent += `
`;
+ } else if (lastAttempt && lastAttempt.status === 'failed') {
+ // DB is fresh but last attempt failed — still worth showing
+ introContent += `
`;
+ introContent += `Last update attempt failed at ${new Date(lastAttempt.timestamp).toLocaleString()}: `;
+ introContent += `${escape(lastAttempt.error || 'Unknown error')}`;
+ introContent += `
`;
}
- introContent += `
`;
- } else if (lastAttempt && lastAttempt.status === 'failed') {
- // DB is fresh but last attempt failed — still worth showing
- introContent += ``;
- introContent += `Last update attempt failed at ${new Date(lastAttempt.timestamp).toLocaleString()}: `;
- introContent += `${escape(lastAttempt.error || 'Unknown error')}`;
- introContent += `
`;
- }
- if (!statsData.cache.loaded) {
- introContent += ``;
- introContent += `Info: Cache is still loading. Some statistics may be incomplete.`;
- introContent += `
`;
- }
+ if (!statsData.cache.loaded) {
+ introContent += ``;
+ introContent += `Info: Cache is still loading. Some statistics may be incomplete.`;
+ introContent += `
`;
+ }
- const fullContent = introContent + content;
+ const fullContent = introContent + content;
- const stats = await gatherPageStatistics();
- stats.processingTime = Date.now() - startTime;
+ const stats = await gatherPageStatistics();
+ stats.processingTime = Date.now() - startTime;
- const html = renderPage('FHIR IG Statistics Status', fullContent, stats);
- res.setHeader('Content-Type', 'text/html');
- res.send(html);
+ const html = renderPage('FHIR IG Statistics Status', fullContent, stats);
+ res.setHeader('Content-Type', 'text/html');
+ res.send(html);
- } catch (error) {
- xigLog.error(`Error generating stats page: ${error.message}`);
- htmlServer.sendErrorResponse(res, 'xig', error);
- }
+ } catch (error) {
+ xigLog.error(`Error generating stats page: ${error.message}`);
+ htmlServer.sendErrorResponse(res, 'xig', error);
+ }
} finally {
globalStats.countRequest('stats', Date.now() - start);
}
@@ -2410,56 +2410,56 @@ router.get('/stats', async (req, res) => {
router.get('/resource/:packagePid/:resourceType/:resourceId', async (req, res) => {
const start = Date.now();
try {
- const startTime = Date.now(); // Add this at the very beginning
- try {
- const { packagePid, resourceType, resourceId } = req.params;
+ const startTime = Date.now(); // Add this at the very beginning
+ try {
+ const { packagePid, resourceType, resourceId } = req.params;
- // Convert URL-safe package PID back to database format (| to #)
- const dbPackagePid = packagePid.replace(/\|/g, '#');
+ // Convert URL-safe package PID back to database format (| to #)
+ const dbPackagePid = packagePid.replace(/\|/g, '#');
- if (!xigDb) {
- throw new Error('Database not available');
- }
+ if (!xigDb) {
+ throw new Error('Database not available');
+ }
- // Get package information first
- const packageObj = getPackageByPid(dbPackagePid);
- if (!packageObj) {
- return res.status(404).send(renderPage('Resource Not Found',
- `Unknown Package: ${escape(packagePid)}
`));
- }
+ // Get package information first
+ const packageObj = getPackageByPid(dbPackagePid);
+ if (!packageObj) {
+ return res.status(404).send(renderPage('Resource Not Found',
+ `Unknown Package: ${escape(packagePid)}
`));
+ }
- // Get resource details
- const resourceQuery = `
- SELECT * FROM Resources
- WHERE PackageKey = ? AND ResourceType = ? AND Id = ?
- `;
+ // Get resource details
+ const resourceQuery = `
+ SELECT * FROM Resources
+ WHERE PackageKey = ? AND ResourceType = ? AND Id = ?
+ `;
- const resourceData = await new Promise((resolve, reject) => {
- xigDb.get(resourceQuery, [packageObj.PackageKey, resourceType, resourceId], (err, row) => {
- if (err) reject(err);
- else resolve(row);
+ const resourceData = await new Promise((resolve, reject) => {
+ xigDb.get(resourceQuery, [packageObj.PackageKey, resourceType, resourceId], (err, row) => {
+ if (err) reject(err);
+ else resolve(row);
+ });
});
- });
- if (!resourceData) {
- return res.status(404).send(renderPage('Resource Not Found',
- `Unknown Resource: ${escape(resourceType)}/${escape(resourceId)} in package ${escape(packagePid)}
`));
- }
+ if (!resourceData) {
+ return res.status(404).send(renderPage('Resource Not Found',
+ `Unknown Resource: ${escape(resourceType)}/${escape(resourceId)} in package ${escape(packagePid)}
`));
+ }
- // Build the resource detail page
- const content = await buildResourceDetailPage(packageObj, resourceData, req.secure);
- const title = `${resourceType}/${resourceId}`;
- const stats = await gatherPageStatistics();
- stats.processingTime = Date.now() - startTime;
+ // Build the resource detail page
+ const content = await buildResourceDetailPage(packageObj, resourceData, req.secure);
+ const title = `${resourceType}/${resourceId}`;
+ const stats = await gatherPageStatistics();
+ stats.processingTime = Date.now() - startTime;
- const html = renderPage(title, content, stats);
- res.setHeader('Content-Type', 'text/html');
- res.send(html);
+ const html = renderPage(title, content, stats);
+ res.setHeader('Content-Type', 'text/html');
+ res.send(html);
- } catch (error) {
- xigLog.error(`Error rendering resource detail page: ${error.message}`);
- htmlServer.sendErrorResponse(res, 'xig', error);
- }
+ } catch (error) {
+ xigLog.error(`Error rendering resource detail page: ${error.message}`);
+ htmlServer.sendErrorResponse(res, 'xig', error);
+ }
} finally {
globalStats.countRequest(':pid', Date.now() - start);
}
@@ -2874,27 +2874,27 @@ router.get('/status', async (req, res) => {
const start = Date.now();
try {
- try {
- const dbInfo = await getDatabaseInfo();
- await res.json({
- status: 'OK',
- database: dbInfo,
- databaseAge: getDatabaseAgeInfo(),
- downloadUrl: XIG_DB_URL,
- localPath: XIG_DB_PATH,
- cache: getCacheStats(),
- updateInProgress: updateInProgress,
- lastUpdateAttempt: getLastUpdateAttempt(),
- updateHistory: getUpdateHistory()
- });
- } catch (error) {
- res.status(500).json({
- status: 'ERROR',
- error: error.message,
- cache: getCacheStats(),
- updateHistory: getUpdateHistory()
- });
- }
+ try {
+ const dbInfo = await getDatabaseInfo();
+ await res.json({
+ status: 'OK',
+ database: dbInfo,
+ databaseAge: getDatabaseAgeInfo(),
+ downloadUrl: XIG_DB_URL,
+ localPath: XIG_DB_PATH,
+ cache: getCacheStats(),
+ updateInProgress: updateInProgress,
+ lastUpdateAttempt: getLastUpdateAttempt(),
+ updateHistory: getUpdateHistory()
+ });
+ } catch (error) {
+ res.status(500).json({
+ status: 'ERROR',
+ error: error.message,
+ cache: getCacheStats(),
+ updateHistory: getUpdateHistory()
+ });
+ }
} finally {
globalStats.countRequest('stats', Date.now() - start);
}
@@ -2929,7 +2929,7 @@ router.post('/update', async (req, res) => {
let globalStats;
// Initialize the XIG module
-async function initializeXigModule(stats) {
+async function initializeXigModule(stats, xigConfig) {
try {
globalStats = stats;
loadTemplate();
@@ -2944,13 +2944,15 @@ async function initializeXigModule(stats) {
}
if (globalStats) {
- globalStats.addTask('XIG Download', describeCron(this.config.crawler.schedule));
+ globalStats.addTask('XIG Download', describeCron('0 2 * * *'));
}
// Check if auto-update is enabled
// Note: This assumes we're called only when XIG is enabled
- cron.schedule('0 2 * * *', () => {
- updateXigDatabase();
- });
+ if (xigConfig?.autoUpdate !== false) {
+ cron.schedule('0 2 * * *', () => {
+ updateXigDatabase();
+ });
+ }
} catch (error) {
xigLog.error(`XIG module initialization failed: ${error.message}`);