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}`);