Sync DNS2 production changes - removed obsolete test suite and refactored structure
This commit is contained in:
@@ -10,7 +10,7 @@ const { execSync } = require('child_process');
|
||||
const path = require('path');
|
||||
const {
|
||||
ValidationError, validateFilePath, validateURL, validateToken,
|
||||
validateServiceConfig, sanitizeString, isValidPort, validateSecurePath,
|
||||
validateServiceConfig, sanitizeString, isValidPort, validateSecurePath
|
||||
} = require('./input-validator');
|
||||
const validatorLib = require('validator');
|
||||
const credentialManager = require('./credential-manager');
|
||||
@@ -128,7 +128,7 @@ licenseManager.loadSecret(LICENSE_SECRET_FILE);
|
||||
// ===== Site configuration loaded from config.json (#5) =====
|
||||
// These are read at startup and refreshed on config save.
|
||||
// All code should use these instead of hardcoded values.
|
||||
const siteConfig = { tld: '.home', caName: '', dnsServerIp: '', dnsServerPort: CADDY.DEFAULT_DNS_PORT, dashboardHost: '', timezone: 'UTC', dnsServers: {}, configurationType: 'homelab', domain: '', routingMode: 'subdomain' };
|
||||
let siteConfig = { tld: '.home', caName: '', dnsServerIp: '', dnsServerPort: CADDY.DEFAULT_DNS_PORT, dashboardHost: '', timezone: 'UTC', dnsServers: {}, configurationType: 'homelab', domain: '', routingMode: 'subdomain' };
|
||||
|
||||
function loadSiteConfig() {
|
||||
try {
|
||||
@@ -147,7 +147,7 @@ function loadSiteConfig() {
|
||||
}
|
||||
|
||||
siteConfig.tld = raw.tld || '.home';
|
||||
if (!siteConfig.tld.startsWith('.')) siteConfig.tld = `.${ siteConfig.tld}`;
|
||||
if (!siteConfig.tld.startsWith('.')) siteConfig.tld = '.' + siteConfig.tld;
|
||||
siteConfig.caName = raw.caName || '';
|
||||
siteConfig.dnsServerIp = (raw.dns && raw.dns.ip) || '';
|
||||
siteConfig.dnsServerPort = (raw.dns && raw.dns.port) || CADDY.DEFAULT_DNS_PORT;
|
||||
@@ -199,7 +199,7 @@ async function callDns(server, apiPath, params) {
|
||||
const response = await fetchT(url, {
|
||||
method: 'GET',
|
||||
headers: { 'Accept': 'application/json' },
|
||||
agent: httpsAgent,
|
||||
agent: httpsAgent
|
||||
}, TIMEOUTS.HTTP_LONG);
|
||||
return response.json();
|
||||
}
|
||||
@@ -323,7 +323,7 @@ async function getServiceById(serviceId) {
|
||||
async function findContainerByName(name, opts = { all: false }) {
|
||||
const containers = await docker.listContainers(opts);
|
||||
const match = containers.find(c =>
|
||||
c.Names.some(n => n.toLowerCase().includes(name.toLowerCase())),
|
||||
c.Names.some(n => n.toLowerCase().includes(name.toLowerCase()))
|
||||
);
|
||||
return match || null;
|
||||
}
|
||||
@@ -348,7 +348,7 @@ async function requireDnsToken(providedToken) {
|
||||
if (providedToken) return providedToken;
|
||||
const result = await ensureValidDnsToken();
|
||||
if (result.success) return result.token;
|
||||
const err = new Error(`No valid DNS token available. ${ result.error}`);
|
||||
const err = new Error('No valid DNS token available. ' + result.error);
|
||||
err.statusCode = 401;
|
||||
throw err;
|
||||
}
|
||||
@@ -430,9 +430,9 @@ async function logError(context, error, additionalInfo = {}) {
|
||||
error: {
|
||||
message: error.message || error,
|
||||
stack: error.stack,
|
||||
code: error.code,
|
||||
code: error.code
|
||||
},
|
||||
...additionalInfo,
|
||||
...additionalInfo
|
||||
};
|
||||
|
||||
// Format log line with request context
|
||||
@@ -446,7 +446,7 @@ async function logError(context, error, additionalInfo = {}) {
|
||||
try {
|
||||
const stats = await fsp.stat(ERROR_LOG_FILE);
|
||||
if (stats.size > MAX_ERROR_LOG_SIZE) {
|
||||
const rotated = `${ERROR_LOG_FILE }.1`;
|
||||
const rotated = ERROR_LOG_FILE + '.1';
|
||||
if (await exists(rotated)) await fsp.unlink(rotated);
|
||||
await fsp.rename(ERROR_LOG_FILE, rotated);
|
||||
}
|
||||
@@ -519,7 +519,7 @@ let tailscaleConfig = {
|
||||
oauthConfigured: false, // true when OAuth credentials are stored
|
||||
tailnet: null, // tailnet name for API calls (e.g., "example.com" or "-")
|
||||
syncInterval: 300, // seconds between API syncs (default 5 min)
|
||||
lastSync: null, // ISO timestamp of last successful sync
|
||||
lastSync: null // ISO timestamp of last successful sync
|
||||
};
|
||||
|
||||
// Load Tailscale config from file
|
||||
@@ -605,7 +605,7 @@ async function getTailscaleAccessToken() {
|
||||
const res = await fetch(TAILSCALE.OAUTH_TOKEN_URL, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: `client_id=${encodeURIComponent(clientId)}&client_secret=${encodeURIComponent(clientSecret)}&grant_type=client_credentials`,
|
||||
body: `client_id=${encodeURIComponent(clientId)}&client_secret=${encodeURIComponent(clientSecret)}&grant_type=client_credentials`
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
@@ -617,7 +617,7 @@ async function getTailscaleAccessToken() {
|
||||
const data = await res.json();
|
||||
_tsTokenCache = {
|
||||
token: data.access_token,
|
||||
expiresAt: Date.now() + (data.expires_in || 3600) * 1000,
|
||||
expiresAt: Date.now() + (data.expires_in || 3600) * 1000
|
||||
};
|
||||
return data.access_token;
|
||||
}
|
||||
@@ -629,7 +629,7 @@ async function syncFromTailscaleAPI() {
|
||||
if (!token || !tailnet) return null;
|
||||
|
||||
const res = await fetch(`${TAILSCALE.API_BASE}/tailnet/${encodeURIComponent(tailnet)}/devices`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
});
|
||||
if (!res.ok) throw new Error(`Tailscale API: HTTP ${res.status}`);
|
||||
|
||||
@@ -647,7 +647,7 @@ async function syncFromTailscaleAPI() {
|
||||
tags: d.tags || [],
|
||||
lastSeen: d.lastSeen,
|
||||
clientVersion: d.clientVersion,
|
||||
isExternal: d.isExternal || false,
|
||||
isExternal: d.isExternal || false
|
||||
}));
|
||||
|
||||
tailscaleConfig.devices = devices;
|
||||
@@ -670,7 +670,7 @@ function startTailscaleSyncTimer() {
|
||||
log.warn('tailscale', 'API sync failed', { error: error.message });
|
||||
}
|
||||
}, interval);
|
||||
log.info('tailscale', 'API sync enabled', { interval: `${interval / 1000 }s` });
|
||||
log.info('tailscale', 'API sync enabled', { interval: interval / 1000 + 's' });
|
||||
}
|
||||
|
||||
function stopTailscaleSyncTimer() {
|
||||
@@ -681,10 +681,10 @@ function stopTailscaleSyncTimer() {
|
||||
}
|
||||
|
||||
// TOTP authentication configuration
|
||||
const totpConfig = {
|
||||
let totpConfig = {
|
||||
enabled: false,
|
||||
sessionDuration: 'never', // 'never' = disabled, or '15m','30m','1h','2h','4h','8h','12h','24h'
|
||||
isSetUp: false, // true once a secret has been verified
|
||||
isSetUp: false // true once a secret has been verified
|
||||
};
|
||||
|
||||
async function loadTotpConfig() {
|
||||
@@ -725,20 +725,20 @@ let notificationConfig = {
|
||||
providers: {
|
||||
discord: { enabled: false, webhookUrl: '' },
|
||||
telegram: { enabled: false, botToken: '', chatId: '' },
|
||||
ntfy: { enabled: false, serverUrl: 'https://ntfy.sh', topic: '' },
|
||||
ntfy: { enabled: false, serverUrl: 'https://ntfy.sh', topic: '' }
|
||||
},
|
||||
events: {
|
||||
containerDown: true,
|
||||
containerUp: true,
|
||||
deploymentSuccess: true,
|
||||
deploymentFailed: true,
|
||||
serviceError: true,
|
||||
serviceError: true
|
||||
},
|
||||
healthCheck: {
|
||||
enabled: false,
|
||||
intervalMinutes: 5,
|
||||
lastCheck: null,
|
||||
},
|
||||
lastCheck: null
|
||||
}
|
||||
};
|
||||
|
||||
// Notification history (in-memory, last 100 entries)
|
||||
@@ -801,7 +801,7 @@ async function saveNotificationConfig() {
|
||||
function addNotificationToHistory(notification) {
|
||||
notificationHistory.unshift({
|
||||
...notification,
|
||||
timestamp: new Date().toISOString(),
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
if (notificationHistory.length > MAX_NOTIFICATION_HISTORY) {
|
||||
notificationHistory = notificationHistory.slice(0, MAX_NOTIFICATION_HISTORY);
|
||||
@@ -817,7 +817,7 @@ async function sendDiscordNotification(title, message, type = 'info') {
|
||||
success: 0x00ff00, // Green
|
||||
error: 0xff0000, // Red
|
||||
warning: 0xffff00, // Yellow
|
||||
info: 0x0099ff, // Blue
|
||||
info: 0x0099ff // Blue
|
||||
};
|
||||
|
||||
const payload = {
|
||||
@@ -826,15 +826,15 @@ async function sendDiscordNotification(title, message, type = 'info') {
|
||||
description: message,
|
||||
color: colors[type] || colors.info,
|
||||
timestamp: new Date().toISOString(),
|
||||
footer: { text: 'DashCaddy Notifications' },
|
||||
}],
|
||||
footer: { text: 'DashCaddy Notifications' }
|
||||
}]
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetchT(webhookUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -857,7 +857,7 @@ async function sendTelegramNotification(title, message, type = 'info') {
|
||||
success: '✅',
|
||||
error: '❌',
|
||||
warning: '⚠️',
|
||||
info: 'ℹ️',
|
||||
info: 'ℹ️'
|
||||
};
|
||||
|
||||
const text = `${emoji[type] || emoji.info} *DashCaddy: ${title}*\n\n${message}`;
|
||||
@@ -869,8 +869,8 @@ async function sendTelegramNotification(title, message, type = 'info') {
|
||||
body: JSON.stringify({
|
||||
chat_id: chatId,
|
||||
text: text,
|
||||
parse_mode: 'Markdown',
|
||||
}),
|
||||
parse_mode: 'Markdown'
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
@@ -894,14 +894,14 @@ async function sendNtfyNotification(title, message, type = 'info') {
|
||||
success: 3, // default
|
||||
error: 5, // max
|
||||
warning: 4, // high
|
||||
info: 3, // default
|
||||
info: 3 // default
|
||||
};
|
||||
|
||||
const tags = {
|
||||
success: 'white_check_mark',
|
||||
error: 'x',
|
||||
warning: 'warning',
|
||||
info: 'information_source',
|
||||
info: 'information_source'
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -910,9 +910,9 @@ async function sendNtfyNotification(title, message, type = 'info') {
|
||||
headers: {
|
||||
'Title': `DashCaddy: ${title}`,
|
||||
'Priority': String(priority[type] || 3),
|
||||
'Tags': tags[type] || 'information_source',
|
||||
'Tags': tags[type] || 'information_source'
|
||||
},
|
||||
body: message,
|
||||
body: message
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -958,14 +958,14 @@ async function sendNotification(event, title, message, type = 'info') {
|
||||
title,
|
||||
message,
|
||||
type,
|
||||
results,
|
||||
results
|
||||
});
|
||||
|
||||
return { sent: true, results };
|
||||
}
|
||||
|
||||
// Container health monitoring state
|
||||
const containerHealthState = {};
|
||||
let containerHealthState = {};
|
||||
let healthCheckInterval = null;
|
||||
|
||||
// Check container health and send notifications
|
||||
@@ -1003,7 +1003,7 @@ async function checkContainerHealth() {
|
||||
'containerUp',
|
||||
'Container Recovered',
|
||||
`**${serviceName}** is now running again.`,
|
||||
'success',
|
||||
'success'
|
||||
);
|
||||
} else {
|
||||
// Container went down
|
||||
@@ -1011,7 +1011,7 @@ async function checkContainerHealth() {
|
||||
'containerDown',
|
||||
'Container Down',
|
||||
`**${serviceName}** has stopped running.\nStatus: ${container.Status}`,
|
||||
'error',
|
||||
'error'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1082,13 +1082,13 @@ const middlewareResult = configureMiddleware(app, {
|
||||
siteConfig, totpConfig, tailscaleConfig,
|
||||
metrics, auditLogger, authManager, log, cryptoUtils,
|
||||
isValidContainerId, isTailscaleIP, getTailscaleStatus,
|
||||
RATE_LIMITS, LIMITS, APP, CACHE_CONFIGS, createCache,
|
||||
RATE_LIMITS, LIMITS, APP, CACHE_CONFIGS, createCache
|
||||
});
|
||||
|
||||
const {
|
||||
strictLimiter, SESSION_DURATIONS, ipSessions,
|
||||
getClientIP, createIPSession, setSessionCookie,
|
||||
clearIPSession, clearSessionCookie, isSessionValid,
|
||||
clearIPSession, clearSessionCookie, isSessionValid
|
||||
} = middlewareResult;
|
||||
|
||||
// ── Populate route context and mount extracted route modules ──
|
||||
@@ -1280,7 +1280,7 @@ app.get('/probe/:id', asyncHandler(async (req, res) => {
|
||||
const fReq = fLib.request({
|
||||
hostname: fp.hostname, port: 443, path: '/', method: 'GET',
|
||||
timeout: 5000, agent: httpsAgent,
|
||||
headers: { 'User-Agent': APP.USER_AGENTS.PROBE },
|
||||
headers: { 'User-Agent': APP.USER_AGENTS.PROBE }
|
||||
}, (fRes) => { fRes.resume(); resolve(fRes.statusCode); });
|
||||
fReq.on('error', reject);
|
||||
fReq.on('timeout', () => { fReq.destroy(); reject(new Error('Timeout')); });
|
||||
@@ -1305,7 +1305,7 @@ app.get('/api/network/ips', (req, res) => {
|
||||
localhost: '127.0.0.1',
|
||||
lan: envLan || null,
|
||||
tailscale: envTailscale || null,
|
||||
all: [],
|
||||
all: []
|
||||
};
|
||||
|
||||
// If env vars not set, try to detect from network interfaces
|
||||
@@ -1364,7 +1364,7 @@ async function refreshDnsToken(username, password, server) {
|
||||
const params = new URLSearchParams({
|
||||
user: username,
|
||||
pass: password,
|
||||
includeInfo: 'false',
|
||||
includeInfo: 'false'
|
||||
});
|
||||
|
||||
const response = await fetchT(
|
||||
@@ -1373,10 +1373,10 @@ async function refreshDnsToken(username, password, server) {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
timeout: 10000,
|
||||
},
|
||||
timeout: 10000
|
||||
}
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
@@ -1436,7 +1436,7 @@ async function ensureValidDnsToken() {
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: 'No DNS credentials configured. Please set up credentials via /api/dns/credentials',
|
||||
error: 'No DNS credentials configured. Please set up credentials via /api/dns/credentials'
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1466,7 +1466,7 @@ async function getTokenForServer(targetServer, role = 'readonly') {
|
||||
const params = new URLSearchParams({
|
||||
user: username,
|
||||
pass: password,
|
||||
includeInfo: 'false',
|
||||
includeInfo: 'false'
|
||||
});
|
||||
|
||||
const response = await fetchT(
|
||||
@@ -1475,9 +1475,9 @@ async function getTokenForServer(targetServer, role = 'readonly') {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
},
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
@@ -1485,7 +1485,7 @@ async function getTokenForServer(targetServer, role = 'readonly') {
|
||||
if (result.status === 'ok' && result.token) {
|
||||
dnsServerTokens.set(cacheKey, {
|
||||
token: result.token,
|
||||
expiry: new Date(Date.now() + SESSION_TTL.DNS_TOKEN).toISOString(),
|
||||
expiry: new Date(Date.now() + SESSION_TTL.DNS_TOKEN).toISOString()
|
||||
});
|
||||
log.info('dns', 'DNS token obtained for server', { server: targetServer, role });
|
||||
return { success: true, token: result.token };
|
||||
@@ -1575,13 +1575,13 @@ function generateCaddyConfig(subdomain, ip, port, options = {}) {
|
||||
}
|
||||
|
||||
if (tailscaleOnly) {
|
||||
config += '\t\t@blocked not remote_ip 100.64.0.0/10';
|
||||
config += `\t\t@blocked not remote_ip 100.64.0.0/10`;
|
||||
if (allowedIPs.length > 0) config += ` ${allowedIPs.join(' ')}`;
|
||||
config += '\n\t\trespond @blocked "Access denied. Tailscale connection required." 403\n';
|
||||
config += `\n\t\trespond @blocked "Access denied. Tailscale connection required." 403\n`;
|
||||
}
|
||||
|
||||
config += `\t\treverse_proxy ${ip}:${port}\n`;
|
||||
config += '\t}';
|
||||
config += `\t}`;
|
||||
return config;
|
||||
}
|
||||
|
||||
@@ -1589,16 +1589,16 @@ function generateCaddyConfig(subdomain, ip, port, options = {}) {
|
||||
let config = `${buildDomain(subdomain)} {\n`;
|
||||
|
||||
if (tailscaleOnly) {
|
||||
config += ' @blocked not remote_ip 100.64.0.0/10';
|
||||
config += ` @blocked not remote_ip 100.64.0.0/10`;
|
||||
if (allowedIPs.length > 0) {
|
||||
config += ` ${allowedIPs.join(' ')}`;
|
||||
}
|
||||
config += '\n respond @blocked "Access denied. Tailscale connection required." 403\n';
|
||||
config += `\n respond @blocked "Access denied. Tailscale connection required." 403\n`;
|
||||
}
|
||||
|
||||
config += ` reverse_proxy ${ip}:${port}\n`;
|
||||
config += ' tls internal\n';
|
||||
config += '}';
|
||||
config += ` tls internal\n`;
|
||||
config += `}`;
|
||||
|
||||
return config;
|
||||
}
|
||||
@@ -1614,7 +1614,7 @@ async function reloadCaddy(content) {
|
||||
const response = await fetchT(`${CADDY_ADMIN_URL}/load`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': CADDY.CONTENT_TYPE },
|
||||
body: content,
|
||||
body: content
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
@@ -1648,7 +1648,7 @@ async function verifySiteAccessible(domain, maxAttempts = 5) {
|
||||
const response = await fetchT(`https://${domain}/`, {
|
||||
method: 'HEAD',
|
||||
agent: httpsAgent, // Ignore cert errors for internal CA
|
||||
timeout: 5000,
|
||||
timeout: 5000
|
||||
});
|
||||
|
||||
// Any response (even 4xx) means Caddy is serving the site
|
||||
@@ -1782,14 +1782,14 @@ app.use((err, req, res, next) => {
|
||||
success: false,
|
||||
error: err.message,
|
||||
code: err.code,
|
||||
...(err.details ? { details: err.details } : {}),
|
||||
...(err.details ? { details: err.details } : {})
|
||||
});
|
||||
}
|
||||
if (err instanceof ValidationError) {
|
||||
return res.status(err.statusCode || 400).json({
|
||||
success: false,
|
||||
error: err.message,
|
||||
errors: err.errors || undefined,
|
||||
errors: err.errors || undefined
|
||||
});
|
||||
}
|
||||
// Catch-all: never leak stack traces or internal paths
|
||||
@@ -1803,150 +1803,150 @@ module.exports = app;
|
||||
|
||||
if (require.main === module) {
|
||||
// Validate configuration and wait for async config loads before starting server
|
||||
(async () => {
|
||||
await Promise.all([_configsReady, _notificationsReady]);
|
||||
await licenseManager.load();
|
||||
await validateStartupConfig({ log, CADDYFILE_PATH, SERVICES_FILE, CONFIG_FILE, CADDY_ADMIN_URL, PORT });
|
||||
|
||||
const server = app.listen(PORT, '0.0.0.0', () => {
|
||||
log.info('server', 'DashCaddy API server started', { port: PORT, caddyfile: CADDYFILE_PATH, caddyAdmin: CADDY_ADMIN_URL, services: SERVICES_FILE });
|
||||
if (BROWSE_ROOTS.length > 0) {
|
||||
log.info('server', 'Media browse roots configured', { roots: BROWSE_ROOTS.map(r => r.hostPath) });
|
||||
}
|
||||
|
||||
// Start new feature modules
|
||||
log.info('server', 'Starting DashCaddy feature modules');
|
||||
|
||||
// Clean up stale port locks
|
||||
(async () => {
|
||||
await Promise.all([_configsReady, _notificationsReady]);
|
||||
await licenseManager.load();
|
||||
await validateStartupConfig({ log, CADDYFILE_PATH, SERVICES_FILE, CONFIG_FILE, CADDY_ADMIN_URL, PORT });
|
||||
|
||||
const server = app.listen(PORT, '0.0.0.0', () => {
|
||||
log.info('server', 'DashCaddy API server started', { port: PORT, caddyfile: CADDYFILE_PATH, caddyAdmin: CADDY_ADMIN_URL, services: SERVICES_FILE });
|
||||
if (BROWSE_ROOTS.length > 0) {
|
||||
log.info('server', 'Media browse roots configured', { roots: BROWSE_ROOTS.map(r => r.hostPath) });
|
||||
}
|
||||
|
||||
// Start new feature modules
|
||||
log.info('server', 'Starting DashCaddy feature modules');
|
||||
|
||||
// Clean up stale port locks
|
||||
(async () => {
|
||||
try {
|
||||
await portLockManager.cleanupStaleLocks();
|
||||
log.info('server', 'Port lock cleanup completed');
|
||||
} catch (error) {
|
||||
log.error('server', 'Port lock cleanup failed', { error: error.message });
|
||||
}
|
||||
})();
|
||||
|
||||
try {
|
||||
resourceMonitor.start();
|
||||
log.info('server', 'Resource monitoring started');
|
||||
} catch (error) {
|
||||
log.error('server', 'Resource monitoring failed to start', { error: error.message });
|
||||
}
|
||||
|
||||
try {
|
||||
backupManager.start();
|
||||
log.info('server', 'Backup manager started');
|
||||
} catch (error) {
|
||||
log.error('server', 'Backup manager failed to start', { error: error.message });
|
||||
}
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
// Auto-configure health checker from services.json
|
||||
await syncHealthCheckerServices({ log, SERVICES_FILE, servicesStateManager, healthChecker, buildServiceUrl, siteConfig, APP });
|
||||
healthChecker.start();
|
||||
log.info('server', 'Health checker started');
|
||||
} catch (error) {
|
||||
log.error('server', 'Health checker failed to start', { error: error.message });
|
||||
}
|
||||
})();
|
||||
|
||||
try {
|
||||
updateManager.start();
|
||||
log.info('server', 'Update manager started');
|
||||
} catch (error) {
|
||||
log.error('server', 'Update manager failed to start', { error: error.message });
|
||||
}
|
||||
|
||||
try {
|
||||
selfUpdater.start();
|
||||
log.info('server', 'Self-updater started', { interval: selfUpdater.config.checkInterval, url: selfUpdater.config.updateUrl });
|
||||
// Check for post-update result (did a previous update succeed or roll back?)
|
||||
selfUpdater.checkPostUpdateResult().then(result => {
|
||||
if (result) {
|
||||
log.info('server', 'Post-update result', result);
|
||||
if (typeof ctx.notification?.send === 'function') {
|
||||
ctx.notification.send('system.update',
|
||||
result.success ? 'DashCaddy Updated' : 'DashCaddy Update Failed',
|
||||
result.success ? `Updated to v${result.version}` : `Update failed: ${result.error || 'Unknown'}. Rolled back.`,
|
||||
result.success ? 'info' : 'error',
|
||||
);
|
||||
}
|
||||
}
|
||||
}).catch(() => {});
|
||||
} catch (error) {
|
||||
log.error('server', 'Self-updater failed to start', { error: error.message });
|
||||
}
|
||||
|
||||
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 (error) {
|
||||
log.error('server', 'Docker maintenance failed to start', { error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
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}`);
|
||||
if (typeof ctx.notification?.send === 'function') {
|
||||
ctx.notification.send('system.digest', 'Daily Log Digest', `Log digest for ${date} is ready. View it in the DashCaddy dashboard.`, 'info');
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
log.error('server', 'Log digest failed to start', { error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
// Tailscale API sync (if OAuth configured)
|
||||
if (tailscaleConfig.oauthConfigured) {
|
||||
startTailscaleSyncTimer();
|
||||
// Run initial sync
|
||||
syncFromTailscaleAPI().catch(e => log.warn('tailscale', 'Initial API sync failed', { error: e.message }));
|
||||
}
|
||||
|
||||
log.info('server', 'All feature modules initialized');
|
||||
});
|
||||
|
||||
// Graceful shutdown — drain connections before exiting
|
||||
function shutdown(signal) {
|
||||
log.info('shutdown', `${signal} received, draining connections...`);
|
||||
resourceMonitor.stop();
|
||||
backupManager.stop();
|
||||
if (dockerMaintenance) dockerMaintenance.stop();
|
||||
if (logDigest) logDigest.stop();
|
||||
healthChecker.stop();
|
||||
updateManager.stop();
|
||||
selfUpdater.stop();
|
||||
stopTailscaleSyncTimer();
|
||||
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();
|
||||
try {
|
||||
await portLockManager.cleanupStaleLocks();
|
||||
log.info('server', 'Port lock cleanup completed');
|
||||
} catch (error) {
|
||||
log.error('server', 'Port lock cleanup failed', { error: error.message });
|
||||
}
|
||||
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
||||
process.on('SIGINT', () => shutdown('SIGINT'));
|
||||
})(); // end async startup
|
||||
})();
|
||||
|
||||
try {
|
||||
resourceMonitor.start();
|
||||
log.info('server', 'Resource monitoring started');
|
||||
} catch (error) {
|
||||
log.error('server', 'Resource monitoring failed to start', { error: error.message });
|
||||
}
|
||||
|
||||
try {
|
||||
backupManager.start();
|
||||
log.info('server', 'Backup manager started');
|
||||
} catch (error) {
|
||||
log.error('server', 'Backup manager failed to start', { error: error.message });
|
||||
}
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
// Auto-configure health checker from services.json
|
||||
await syncHealthCheckerServices({ log, SERVICES_FILE, servicesStateManager, healthChecker, buildServiceUrl, siteConfig, APP });
|
||||
healthChecker.start();
|
||||
log.info('server', 'Health checker started');
|
||||
} catch (error) {
|
||||
log.error('server', 'Health checker failed to start', { error: error.message });
|
||||
}
|
||||
})();
|
||||
|
||||
try {
|
||||
updateManager.start();
|
||||
log.info('server', 'Update manager started');
|
||||
} catch (error) {
|
||||
log.error('server', 'Update manager failed to start', { error: error.message });
|
||||
}
|
||||
|
||||
try {
|
||||
selfUpdater.start();
|
||||
log.info('server', 'Self-updater started', { interval: selfUpdater.config.checkInterval, url: selfUpdater.config.updateUrl });
|
||||
// Check for post-update result (did a previous update succeed or roll back?)
|
||||
selfUpdater.checkPostUpdateResult().then(result => {
|
||||
if (result) {
|
||||
log.info('server', 'Post-update result', result);
|
||||
if (typeof ctx.notification?.send === 'function') {
|
||||
ctx.notification.send('system.update',
|
||||
result.success ? 'DashCaddy Updated' : 'DashCaddy Update Failed',
|
||||
result.success ? `Updated to v${result.version}` : `Update failed: ${result.error || 'Unknown'}. Rolled back.`,
|
||||
result.success ? 'info' : 'error'
|
||||
);
|
||||
}
|
||||
}
|
||||
}).catch(() => {});
|
||||
} catch (error) {
|
||||
log.error('server', 'Self-updater failed to start', { error: error.message });
|
||||
}
|
||||
|
||||
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 (error) {
|
||||
log.error('server', 'Docker maintenance failed to start', { error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
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}`);
|
||||
if (typeof ctx.notification?.send === 'function') {
|
||||
ctx.notification.send('system.digest', 'Daily Log Digest', `Log digest for ${date} is ready. View it in the DashCaddy dashboard.`, 'info');
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
log.error('server', 'Log digest failed to start', { error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
// Tailscale API sync (if OAuth configured)
|
||||
if (tailscaleConfig.oauthConfigured) {
|
||||
startTailscaleSyncTimer();
|
||||
// Run initial sync
|
||||
syncFromTailscaleAPI().catch(e => log.warn('tailscale', 'Initial API sync failed', { error: e.message }));
|
||||
}
|
||||
|
||||
log.info('server', 'All feature modules initialized');
|
||||
});
|
||||
|
||||
// Graceful shutdown — drain connections before exiting
|
||||
function shutdown(signal) {
|
||||
log.info('shutdown', `${signal} received, draining connections...`);
|
||||
resourceMonitor.stop();
|
||||
backupManager.stop();
|
||||
if (dockerMaintenance) dockerMaintenance.stop();
|
||||
if (logDigest) logDigest.stop();
|
||||
healthChecker.stop();
|
||||
updateManager.stop();
|
||||
selfUpdater.stop();
|
||||
stopTailscaleSyncTimer();
|
||||
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'));
|
||||
})(); // end async startup
|
||||
} // end if (require.main === module)
|
||||
|
||||
// #2: Catch unhandled errors so the process doesn't crash silently
|
||||
|
||||
Reference in New Issue
Block a user