Committed by Hermes autonomous QA sprint 2026-08-13. These files were modified during the Aug 12 sprint but never committed.
327 lines
12 KiB
JavaScript
327 lines
12 KiB
JavaScript
/**
|
|
* DashCaddy API Server - Entry Point
|
|
* Minimal startup script - all logic moved to src/
|
|
*/
|
|
const { createApp } = require('./src/app');
|
|
const { fetchT } = require('./src/utils/http');
|
|
const platformPaths = require('./platform-paths');
|
|
|
|
// Unhandled error handlers
|
|
process.on('unhandledRejection', (reason) => {
|
|
console.error('[FATAL] Unhandled Promise Rejection:', reason);
|
|
process.exit(1);
|
|
});
|
|
|
|
process.on('uncaughtException', (error) => {
|
|
console.error('[FATAL] Uncaught Exception:', error);
|
|
setTimeout(() => process.exit(1), 1000).unref();
|
|
});
|
|
|
|
// Main startup
|
|
(async () => {
|
|
try {
|
|
// Create and configure Express app
|
|
const { app, log, config, licenseManager } = await createApp();
|
|
|
|
// Load license
|
|
await licenseManager.load();
|
|
|
|
const PORT = parseInt(process.env.PORT, 10) || 3001;
|
|
const HOST = process.env.HOST || '0.0.0.0';
|
|
const CADDYFILE_PATH = process.env.CADDYFILE_PATH || platformPaths.caddyfile;
|
|
const CADDY_ADMIN_URL = process.env.CADDY_ADMIN_URL || platformPaths.caddyAdminUrl;
|
|
const SERVICES_FILE = process.env.SERVICES_FILE || platformPaths.servicesFile;
|
|
const CONFIG_FILE = process.env.CONFIG_FILE || platformPaths.servicesFile.replace('services.json', 'config.json');
|
|
|
|
// dataDir safety guard — DC-046 follow-up to DC-039. Refuse to boot in
|
|
// production if dataDir resolved into the Docker image layer (audit-log,
|
|
// license keys, error logs etc. would silently land there and vanish on
|
|
// the next container recreate). Throws → no crash-loop, just a clear
|
|
// fatal error message before any runtime state can be written.
|
|
platformPaths.assertSafe({ mode: process.env.NODE_ENV === 'production' ? 'production' : 'development' });
|
|
|
|
// Validate startup configuration
|
|
const { validateStartupConfig } = require('./src/utilities/startup-validator');
|
|
await validateStartupConfig({
|
|
log,
|
|
CADDYFILE_PATH,
|
|
SERVICES_FILE,
|
|
CONFIG_FILE,
|
|
CADDY_ADMIN_URL,
|
|
PORT
|
|
});
|
|
|
|
// Start HTTP server
|
|
const server = app.listen(PORT, HOST, () => {
|
|
log.info('server', 'DashCaddy API server started', {
|
|
port: PORT,
|
|
host: HOST,
|
|
caddyfile: CADDYFILE_PATH,
|
|
caddyAdmin: CADDY_ADMIN_URL,
|
|
services: SERVICES_FILE,
|
|
environment: process.env.NODE_ENV || 'production'
|
|
});
|
|
|
|
// Attach WebSocket exec handler (with auth)
|
|
const attachExecWS = require('./routes/exec');
|
|
const authManager = require('./src/managers/auth-manager');
|
|
attachExecWS(server, log, authManager);
|
|
log.info('server', 'WebSocket exec handler attached (auth enforced)');
|
|
|
|
// DC-076: Attach dashboard WebSocket for real-time updates
|
|
try {
|
|
const createDashboardWS = require('./src/websocket/dashboard-ws');
|
|
const resourceMonitor = require('./src/managers/resource-monitor');
|
|
const healthChecker = require('./src/monitoring/health-checker');
|
|
const updateManager = require('./src/managers/update-manager');
|
|
const dependencyManager = require('./src/managers/dependency-manager');
|
|
const autoRestartManager = require('./src/managers/auto-restart-manager');
|
|
const configDriftDetector = require('./src/managers/config-drift-detector');
|
|
const sslMonitor = require('./src/monitoring/ssl-monitor');
|
|
|
|
createDashboardWS(server, {
|
|
resourceMonitor,
|
|
healthChecker,
|
|
updateManager,
|
|
dependencyManager,
|
|
autoRestartManager,
|
|
driftDetector: configDriftDetector,
|
|
sslMonitor,
|
|
log,
|
|
});
|
|
log.info('server', 'Dashboard WebSocket attached at /api/v1/ws');
|
|
} catch (err) {
|
|
log.error('server', 'Dashboard WebSocket failed to attach', { error: err.message });
|
|
}
|
|
|
|
// Start feature modules
|
|
const resourceMonitor = require('./src/managers/resource-monitor');
|
|
const backupManager = require('./src/utilities/backup-manager');
|
|
const healthChecker = require('./src/monitoring/health-checker');
|
|
const updateManager = require('./src/managers/update-manager');
|
|
const selfUpdater = require('./src/docker/self-updater');
|
|
const portLockManager = require('./src/managers/port-lock-manager');
|
|
|
|
// Create servicesStateManager early — needed by workflow engine init
|
|
const StateManager = require('./src/managers/state-manager');
|
|
const servicesStateManager = new StateManager(SERVICES_FILE);
|
|
|
|
// Optional modules
|
|
let dockerMaintenance, logDigest, bundledWorkflows;
|
|
try { dockerMaintenance = require('./src/docker/docker-maintenance'); } catch { /* optional */ }
|
|
try { logDigest = require('./src/security/log-digest'); } catch { /* optional */ }
|
|
try { bundledWorkflows = require('./src/recipes/bundled-workflows'); } catch { /* optional */ }
|
|
|
|
// Initialize workflow engine if bundled-workflows is available
|
|
// NOTE: createApp() already initializes the workflow engine in src/app.js
|
|
// This block is kept for backward compat with entry points that don't use createApp()
|
|
let workflowEngine = null;
|
|
if (bundledWorkflows) {
|
|
try {
|
|
const { fetchT } = require('./src/utils/http');
|
|
const { WorkflowEngine } = bundledWorkflows;
|
|
// Create a context with needed services
|
|
const workflowCtx = {
|
|
docker: { client: require('dockerode')() },
|
|
notification: new (require('./src/managers/notification-manager'))({
|
|
NOTIFICATIONS_FILE: process.env.NOTIFICATIONS_FILE || require('./platform-paths').notificationsFile,
|
|
fetchT,
|
|
log,
|
|
config
|
|
}),
|
|
backupManager,
|
|
resourceMonitor,
|
|
servicesStateManager
|
|
};
|
|
workflowEngine = new WorkflowEngine(workflowCtx);
|
|
log.info('server', 'Workflow engine initialized');
|
|
} catch (err) {
|
|
log.error('server', 'Workflow engine failed to initialize', { error: err.message });
|
|
}
|
|
}
|
|
|
|
log.info('server', 'Starting feature modules');
|
|
|
|
// Clean up stale port locks
|
|
portLockManager.cleanupStaleLocks()
|
|
.then(() => log.info('server', 'Port lock cleanup completed'))
|
|
.catch(err => log.error('server', 'Port lock cleanup failed', { error: err.message }));
|
|
|
|
// Resource monitoring
|
|
try {
|
|
resourceMonitor.start();
|
|
// Connect workflow engine to resource monitor for resource-alert events
|
|
if (workflowEngine) {
|
|
resourceMonitor.setWorkflowEngine(workflowEngine);
|
|
}
|
|
log.info('server', 'Resource monitoring started');
|
|
} catch (err) {
|
|
log.error('server', 'Resource monitoring failed to start', { error: err.message });
|
|
}
|
|
|
|
// Backup manager
|
|
try {
|
|
backupManager.start();
|
|
log.info('server', 'Backup manager started');
|
|
} catch (err) {
|
|
log.error('server', 'Backup manager failed to start', { error: err.message });
|
|
}
|
|
|
|
// Security event workers (Caddy access log, fail2ban, shared_bans)
|
|
// Each one tail-follows a log file and emits events into the unified
|
|
// security store. They survive restarts via persisted offsets.
|
|
try {
|
|
const { startAll: startSecurityWorkers } = require('./src/security/event-workers');
|
|
startSecurityWorkers({ log });
|
|
log.info('server', 'Security event workers started');
|
|
} catch (err) {
|
|
log.error('server', 'Security event workers failed to start', { error: err.message });
|
|
}
|
|
|
|
// Connect workflow engine to update manager for pre-update events
|
|
if (workflowEngine) {
|
|
updateManager.setWorkflowEngine(workflowEngine);
|
|
}
|
|
|
|
// Health checker (with service sync)
|
|
(async () => {
|
|
try {
|
|
const { syncHealthCheckerServices } = require('./src/utilities/startup-validator');
|
|
const StateManager = require('./src/managers/state-manager');
|
|
const servicesStateManager = new StateManager(SERVICES_FILE);
|
|
|
|
|
|
await syncHealthCheckerServices({
|
|
log,
|
|
SERVICES_FILE,
|
|
servicesStateManager,
|
|
healthChecker,
|
|
buildServiceUrl: (subdomain) => config.routingMode === 'subdirectory' && config.domain
|
|
? `https://${config.domain}/${subdomain}`
|
|
: `https://${subdomain}${config.tld}`,
|
|
siteConfig: config,
|
|
APP: require('./src/utilities/constants').APP
|
|
});
|
|
|
|
healthChecker.start();
|
|
log.info('server', 'Health checker started');
|
|
} catch (err) {
|
|
log.error('server', 'Health checker failed to start', { error: err.message });
|
|
}
|
|
})();
|
|
|
|
// Update manager
|
|
try {
|
|
updateManager.start();
|
|
log.info('server', 'Update manager started');
|
|
} catch (err) {
|
|
log.error('server', 'Update manager failed to start', { error: err.message });
|
|
}
|
|
|
|
// Self-updater
|
|
try {
|
|
selfUpdater.start();
|
|
log.info('server', 'Self-updater started', {
|
|
interval: selfUpdater.config.checkInterval,
|
|
url: selfUpdater.config.updateUrl
|
|
});
|
|
|
|
selfUpdater.checkPostUpdateResult()
|
|
.then(result => {
|
|
if (result) {
|
|
log.info('server', 'Post-update result', result);
|
|
}
|
|
})
|
|
.catch(() => {});
|
|
} catch (err) {
|
|
log.error('server', 'Self-updater failed to start', { error: err.message });
|
|
}
|
|
|
|
// Docker maintenance (optional)
|
|
if (dockerMaintenance) {
|
|
try {
|
|
dockerMaintenance.start();
|
|
log.info('server', 'Docker maintenance started');
|
|
|
|
dockerMaintenance.on('maintenance-complete', (result) => {
|
|
const saved = Math.round(result.spaceReclaimed.total / 1024 / 1024);
|
|
if (saved > 0 || result.warnings.length > 0) {
|
|
log.info('maintenance', 'Docker maintenance completed', {
|
|
spaceReclaimedMB: saved,
|
|
pruned: result.pruned,
|
|
warnings: result.warnings.length
|
|
});
|
|
}
|
|
if (result.warnings.length > 0) {
|
|
for (const w of result.warnings) log.warn('maintenance', w);
|
|
}
|
|
});
|
|
} catch (err) {
|
|
log.error('server', 'Docker maintenance failed to start', { error: err.message });
|
|
}
|
|
}
|
|
|
|
// Log digest (optional)
|
|
if (logDigest) {
|
|
try {
|
|
logDigest.start(platformPaths.digestDir);
|
|
log.info('server', 'Log digest started', { digestDir: platformPaths.digestDir });
|
|
|
|
logDigest.on('digest-generated', ({ date }) => {
|
|
log.info('digest', `Daily digest generated for ${date}`);
|
|
});
|
|
} catch (err) {
|
|
log.error('server', 'Log digest failed to start', { error: err.message });
|
|
}
|
|
}
|
|
|
|
log.info('server', 'All feature modules initialized');
|
|
});
|
|
|
|
// Graceful shutdown
|
|
const shutdown = (signal) => {
|
|
log.info('shutdown', `${signal} received, draining connections...`);
|
|
|
|
const resourceMonitor = require('./src/managers/resource-monitor');
|
|
const backupManager = require('./src/utilities/backup-manager');
|
|
const healthChecker = require('./src/monitoring/health-checker');
|
|
const updateManager = require('./src/managers/update-manager');
|
|
const selfUpdater = require('./src/docker/self-updater');
|
|
|
|
resourceMonitor.stop();
|
|
backupManager.stop();
|
|
healthChecker.stop();
|
|
updateManager.stop();
|
|
selfUpdater.stop();
|
|
|
|
try {
|
|
const dockerMaintenance = require('./src/docker/docker-maintenance');
|
|
dockerMaintenance.stop();
|
|
} catch { /* optional */ }
|
|
|
|
try {
|
|
const logDigest = require('./src/security/log-digest');
|
|
logDigest.stop();
|
|
} catch { /* optional */ }
|
|
|
|
server.close(() => {
|
|
log.info('shutdown', 'HTTP server closed');
|
|
process.exit(0);
|
|
});
|
|
|
|
// Force exit after 5s if connections don't drain
|
|
setTimeout(() => process.exit(0), 5000).unref();
|
|
};
|
|
|
|
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
|
|
} catch (error) {
|
|
console.error('[FATAL] Server startup failed:', error);
|
|
process.exit(1);
|
|
}
|
|
})();
|
|
|
|
// Export for testing
|
|
module.exports = require('./src/app');
|