Phase 1: Add ESLint/Prettier config + baseline auto-fixes

This commit is contained in:
Krystie
2026-03-22 11:00:25 +01:00
parent 41a0cdee7e
commit e2c67a8fe8
90 changed files with 4008 additions and 3066 deletions

View File

@@ -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.
let siteConfig = { tld: '.home', caName: '', dnsServerIp: '', dnsServerPort: CADDY.DEFAULT_DNS_PORT, dashboardHost: '', timezone: 'UTC', dnsServers: {}, configurationType: 'homelab', domain: '', routingMode: 'subdomain' };
const 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
let totpConfig = {
const 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
let containerHealthState = {};
const 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 () => {
try {
await portLockManager.cleanupStaleLocks();
log.info('server', 'Port lock cleanup completed');
} catch (error) {
log.error('server', 'Port lock cleanup failed', { error: error.message });
}
})();
await Promise.all([_configsReady, _notificationsReady]);
await licenseManager.load();
await validateStartupConfig({ log, CADDYFILE_PATH, SERVICES_FILE, CONFIG_FILE, CADDY_ADMIN_URL, PORT });
try {
resourceMonitor.start();
log.info('server', 'Resource monitoring started');
} catch (error) {
log.error('server', 'Resource monitoring failed to start', { error: error.message });
}
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) });
}
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 });
}
// Start new feature modules
log.info('server', 'Starting DashCaddy feature modules');
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'
);
// 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 });
}
}
}).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 (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 });
}
if (result.warnings.length > 0) {
for (const w of result.warnings) log.warn('maintenance', w);
}
}
// 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);
});
} catch (error) {
log.error('server', 'Docker maintenance failed to start', { error: error.message });
// Force exit after 5s if connections don't drain
setTimeout(() => process.exit(0), 5000).unref();
}
}
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
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