Unify URL resolution, add health checker sync, and make modules optional
- Add url-resolver.js with single resolveServiceUrl() used by all 5 consumers (probes, health routes, health checker auto-config) - Health checker now does full sync (add/update/remove) instead of add-only, and re-syncs automatically after every services.json mutation - docker-maintenance and log-digest are now optional imports with try/catch, preventing container crashes when these files are absent - Add null guards in routes/logs.js for graceful 503 responses Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -111,10 +111,12 @@ const ctx = {
|
|||||||
logError: null,
|
logError: null,
|
||||||
safeErrorMessage: null,
|
safeErrorMessage: null,
|
||||||
buildDomain: null,
|
buildDomain: null,
|
||||||
|
buildServiceUrl: null,
|
||||||
getServiceById: null,
|
getServiceById: null,
|
||||||
readConfig: null,
|
readConfig: null,
|
||||||
saveConfig: null,
|
saveConfig: null,
|
||||||
addServiceToConfig: null,
|
addServiceToConfig: null,
|
||||||
|
resyncHealthChecker: null,
|
||||||
validateURL: null,
|
validateURL: null,
|
||||||
|
|
||||||
// Middleware
|
// Middleware
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const { TIMEOUTS } = require('../constants');
|
|||||||
const { exists } = require('../fs-helpers');
|
const { exists } = require('../fs-helpers');
|
||||||
const { paginate, parsePaginationParams } = require('../pagination');
|
const { paginate, parsePaginationParams } = require('../pagination');
|
||||||
const platformPaths = require('../platform-paths');
|
const platformPaths = require('../platform-paths');
|
||||||
|
const { resolveServiceUrl } = require('../url-resolver');
|
||||||
|
|
||||||
module.exports = function(ctx) {
|
module.exports = function(ctx) {
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
@@ -36,18 +37,7 @@ module.exports = function(ctx) {
|
|||||||
let checkType = 'http';
|
let checkType = 'http';
|
||||||
|
|
||||||
// Determine URL to check
|
// Determine URL to check
|
||||||
if (service.isExternal && service.externalUrl) {
|
url = resolveServiceUrl(serviceId, service, ctx.siteConfig, ctx.buildServiceUrl);
|
||||||
url = service.externalUrl;
|
|
||||||
} else if (service.containerId || service.containerName) {
|
|
||||||
// Local container - check via localhost and port
|
|
||||||
const port = service.port || 80;
|
|
||||||
url = `http://localhost:${port}`;
|
|
||||||
} else if (service.url) {
|
|
||||||
url = service.url.startsWith('http') ? service.url : `https://${service.url}`;
|
|
||||||
} else if (service.id) {
|
|
||||||
// Try common URL pattern
|
|
||||||
url = `https://${ctx.buildDomain(service.id)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!url) {
|
if (!url) {
|
||||||
health[serviceId] = { status: 'unknown', reason: 'No URL configured' };
|
health[serviceId] = { status: 'unknown', reason: 'No URL configured' };
|
||||||
@@ -157,14 +147,7 @@ module.exports = function(ctx) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Determine URL
|
// Determine URL
|
||||||
let url = null;
|
const url = resolveServiceUrl(serviceId, service, ctx.siteConfig, ctx.buildServiceUrl);
|
||||||
if (service.isExternal && service.externalUrl) {
|
|
||||||
url = service.externalUrl;
|
|
||||||
} else if (service.url) {
|
|
||||||
url = service.url.startsWith('http') ? service.url : `https://${service.url}`;
|
|
||||||
} else {
|
|
||||||
url = `https://${ctx.buildDomain(serviceId)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check health
|
// Check health
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ module.exports = function(ctx) {
|
|||||||
|
|
||||||
// Get latest daily digest
|
// Get latest daily digest
|
||||||
router.get('/logs/digest/latest', ctx.asyncHandler(async (req, res) => {
|
router.get('/logs/digest/latest', ctx.asyncHandler(async (req, res) => {
|
||||||
|
if (!ctx.logDigest) return ctx.errorResponse(res, 503, 'Log digest not available');
|
||||||
const digest = await ctx.logDigest.getLatestDigest();
|
const digest = await ctx.logDigest.getLatestDigest();
|
||||||
if (!digest) {
|
if (!digest) {
|
||||||
return res.json({ success: true, digest: null, message: 'No digest available yet. First digest is generated at midnight.' });
|
return res.json({ success: true, digest: null, message: 'No digest available yet. First digest is generated at midnight.' });
|
||||||
@@ -149,18 +150,21 @@ module.exports = function(ctx) {
|
|||||||
|
|
||||||
// Get live digest data (today's accumulated stats)
|
// Get live digest data (today's accumulated stats)
|
||||||
router.get('/logs/digest/live', ctx.asyncHandler(async (req, res) => {
|
router.get('/logs/digest/live', ctx.asyncHandler(async (req, res) => {
|
||||||
|
if (!ctx.logDigest) return ctx.errorResponse(res, 503, 'Log digest not available');
|
||||||
const live = ctx.logDigest.getLiveData();
|
const live = ctx.logDigest.getLiveData();
|
||||||
res.json({ success: true, ...live });
|
res.json({ success: true, ...live });
|
||||||
}, 'logs-digest-live'));
|
}, 'logs-digest-live'));
|
||||||
|
|
||||||
// List available digest dates
|
// List available digest dates
|
||||||
router.get('/logs/digest/history', ctx.asyncHandler(async (req, res) => {
|
router.get('/logs/digest/history', ctx.asyncHandler(async (req, res) => {
|
||||||
|
if (!ctx.logDigest) return ctx.errorResponse(res, 503, 'Log digest not available');
|
||||||
const dates = await ctx.logDigest.listDigests();
|
const dates = await ctx.logDigest.listDigests();
|
||||||
res.json({ success: true, dates });
|
res.json({ success: true, dates });
|
||||||
}, 'logs-digest-history'));
|
}, 'logs-digest-history'));
|
||||||
|
|
||||||
// Generate digest on demand (for today or a specific date)
|
// Generate digest on demand (for today or a specific date)
|
||||||
router.post('/logs/digest/generate', ctx.asyncHandler(async (req, res) => {
|
router.post('/logs/digest/generate', ctx.asyncHandler(async (req, res) => {
|
||||||
|
if (!ctx.logDigest) return ctx.errorResponse(res, 503, 'Log digest not available');
|
||||||
const date = req.body.date || new Date().toISOString().slice(0, 10);
|
const date = req.body.date || new Date().toISOString().slice(0, 10);
|
||||||
const digest = await ctx.logDigest.generateDailyDigest(date);
|
const digest = await ctx.logDigest.generateDailyDigest(date);
|
||||||
res.json({ success: true, digest });
|
res.json({ success: true, digest });
|
||||||
@@ -168,6 +172,7 @@ module.exports = function(ctx) {
|
|||||||
|
|
||||||
// Get digest for a specific date (JSON)
|
// Get digest for a specific date (JSON)
|
||||||
router.get('/logs/digest/:date', ctx.asyncHandler(async (req, res) => {
|
router.get('/logs/digest/:date', ctx.asyncHandler(async (req, res) => {
|
||||||
|
if (!ctx.logDigest) return ctx.errorResponse(res, 503, 'Log digest not available');
|
||||||
const { date } = req.params;
|
const { date } = req.params;
|
||||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) {
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) {
|
||||||
return ctx.errorResponse(res, 400, 'Invalid date format. Use YYYY-MM-DD.');
|
return ctx.errorResponse(res, 400, 'Invalid date format. Use YYYY-MM-DD.');
|
||||||
@@ -186,6 +191,7 @@ module.exports = function(ctx) {
|
|||||||
|
|
||||||
// Get Docker disk usage snapshot
|
// Get Docker disk usage snapshot
|
||||||
router.get('/logs/docker-disk', ctx.asyncHandler(async (req, res) => {
|
router.get('/logs/docker-disk', ctx.asyncHandler(async (req, res) => {
|
||||||
|
if (!ctx.dockerMaintenance) return ctx.errorResponse(res, 503, 'Docker maintenance not available');
|
||||||
const diskUsage = await ctx.dockerMaintenance.getDiskUsage();
|
const diskUsage = await ctx.dockerMaintenance.getDiskUsage();
|
||||||
const status = ctx.dockerMaintenance.getStatus();
|
const status = ctx.dockerMaintenance.getStatus();
|
||||||
res.json({ success: true, diskUsage, maintenance: status });
|
res.json({ success: true, diskUsage, maintenance: status });
|
||||||
@@ -193,6 +199,7 @@ module.exports = function(ctx) {
|
|||||||
|
|
||||||
// Trigger Docker maintenance manually
|
// Trigger Docker maintenance manually
|
||||||
router.post('/logs/docker-maintenance', ctx.asyncHandler(async (req, res) => {
|
router.post('/logs/docker-maintenance', ctx.asyncHandler(async (req, res) => {
|
||||||
|
if (!ctx.dockerMaintenance) return ctx.errorResponse(res, 503, 'Docker maintenance not available');
|
||||||
const result = await ctx.dockerMaintenance.runMaintenance();
|
const result = await ctx.dockerMaintenance.runMaintenance();
|
||||||
res.json({ success: true, result });
|
res.json({ success: true, result });
|
||||||
}, 'logs-docker-maintenance'));
|
}, 'logs-docker-maintenance'));
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const { APP, REGEX, TIMEOUTS } = require('../constants');
|
|||||||
const { validateServiceConfig, isValidPort } = require('../input-validator');
|
const { validateServiceConfig, isValidPort } = require('../input-validator');
|
||||||
const { exists } = require('../fs-helpers');
|
const { exists } = require('../fs-helpers');
|
||||||
const { paginate, parsePaginationParams } = require('../pagination');
|
const { paginate, parsePaginationParams } = require('../pagination');
|
||||||
|
const { resolveServiceUrl } = require('../url-resolver');
|
||||||
|
|
||||||
module.exports = function(ctx) {
|
module.exports = function(ctx) {
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
@@ -33,10 +34,7 @@ module.exports = function(ctx) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resolveProbeUrl(id, service) {
|
function resolveProbeUrl(id, service) {
|
||||||
if (id === 'internet') return 'https://www.google.com';
|
return resolveServiceUrl(id, service, ctx.siteConfig, ctx.buildServiceUrl);
|
||||||
if (service?.isExternal && service.externalUrl) return service.externalUrl;
|
|
||||||
if (service?.url) return service.url.startsWith('http') ? service.url : `https://${service.url}`;
|
|
||||||
return ctx.buildServiceUrl(id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function requestStatusCode(url, method) {
|
function requestStatusCode(url, method) {
|
||||||
@@ -308,6 +306,7 @@ module.exports = function(ctx) {
|
|||||||
return services;
|
return services;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ctx.resyncHealthChecker?.().catch(() => {});
|
||||||
res.json({ success: true, message: `Service "${name}" added to dashboard` });
|
res.json({ success: true, message: `Service "${name}" added to dashboard` });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ctx.log.error('deploy', 'Error adding service', { error: error.message });
|
ctx.log.error('deploy', 'Error adding service', { error: error.message });
|
||||||
@@ -339,6 +338,7 @@ module.exports = function(ctx) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await ctx.servicesStateManager.write(services);
|
await ctx.servicesStateManager.write(services);
|
||||||
|
ctx.resyncHealthChecker?.().catch(() => {});
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
@@ -367,6 +367,7 @@ module.exports = function(ctx) {
|
|||||||
return ctx.errorResponse(res, 404, `Service "${id}" not found`);
|
return ctx.errorResponse(res, 404, `Service "${id}" not found`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctx.resyncHealthChecker?.().catch(() => {});
|
||||||
res.json({ success: true, message: `Service "${id}" removed from dashboard` });
|
res.json({ success: true, message: `Service "${id}" removed from dashboard` });
|
||||||
}, 'services-delete'));
|
}, 'services-delete'));
|
||||||
|
|
||||||
@@ -454,6 +455,7 @@ module.exports = function(ctx) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctx.resyncHealthChecker?.().catch(() => {});
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: `Service updated: ${oldSubdomain} -> ${newSubdomain}`,
|
message: `Service updated: ${oldSubdomain} -> ${newSubdomain}`,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ const credentialManager = require('./credential-manager');
|
|||||||
const { CACHE_CONFIGS, createCache } = require('./cache-config');
|
const { CACHE_CONFIGS, createCache } = require('./cache-config');
|
||||||
const { AppError } = require('./errors');
|
const { AppError } = require('./errors');
|
||||||
const { validateConfig } = require('./config-schema');
|
const { validateConfig } = require('./config-schema');
|
||||||
|
const { resolveServiceUrl } = require('./url-resolver');
|
||||||
const {
|
const {
|
||||||
APP, APP_PORTS, ARR_SERVICES, TIMEOUTS, RETRIES,
|
APP, APP_PORTS, ARR_SERVICES, TIMEOUTS, RETRIES,
|
||||||
SESSION_TTL, RATE_LIMITS, CADDY, PLEX, LIMITS,
|
SESSION_TTL, RATE_LIMITS, CADDY, PLEX, LIMITS,
|
||||||
@@ -50,8 +51,10 @@ const backupManager = require('./backup-manager');
|
|||||||
const healthChecker = require('./health-checker');
|
const healthChecker = require('./health-checker');
|
||||||
const updateManager = require('./update-manager');
|
const updateManager = require('./update-manager');
|
||||||
const selfUpdater = require('./self-updater');
|
const selfUpdater = require('./self-updater');
|
||||||
const dockerMaintenance = require('./docker-maintenance');
|
let dockerMaintenance;
|
||||||
const logDigest = require('./log-digest');
|
try { dockerMaintenance = require('./docker-maintenance'); } catch (_) { console.warn('[WARN] docker-maintenance module not found, skipped'); }
|
||||||
|
let logDigest;
|
||||||
|
try { logDigest = require('./log-digest'); } catch (_) { console.warn('[WARN] log-digest module not found, skipped'); }
|
||||||
const StateManager = require('./state-manager');
|
const StateManager = require('./state-manager');
|
||||||
const auditLogger = require('./audit-logger');
|
const auditLogger = require('./audit-logger');
|
||||||
const portLockManager = require('./port-lock-manager');
|
const portLockManager = require('./port-lock-manager');
|
||||||
@@ -1173,6 +1176,7 @@ Object.assign(ctx, {
|
|||||||
loadDnsCredentials: () => {},
|
loadDnsCredentials: () => {},
|
||||||
SERVICES_FILE, CONFIG_FILE, TOTP_CONFIG_FILE, TAILSCALE_CONFIG_FILE,
|
SERVICES_FILE, CONFIG_FILE, TOTP_CONFIG_FILE, TAILSCALE_CONFIG_FILE,
|
||||||
NOTIFICATIONS_FILE, ERROR_LOG_FILE,
|
NOTIFICATIONS_FILE, ERROR_LOG_FILE,
|
||||||
|
resyncHealthChecker: () => syncHealthCheckerServices({ log, SERVICES_FILE, servicesStateManager, healthChecker, buildServiceUrl, siteConfig, APP }),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Build versioned API router — all route modules attach here
|
// Build versioned API router — all route modules attach here
|
||||||
@@ -1225,30 +1229,16 @@ app.get('/probe/:id', asyncHandler(async (req, res) => {
|
|||||||
const id = req.params.id;
|
const id = req.params.id;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let url;
|
// Look up service in services.json
|
||||||
|
let service = null;
|
||||||
if (id === 'internet') {
|
if (id !== 'internet' && await exists(SERVICES_FILE)) {
|
||||||
// Internet connectivity check
|
const data = await servicesStateManager.read();
|
||||||
url = 'https://www.google.com';
|
const services = Array.isArray(data) ? data : data.services || [];
|
||||||
} else {
|
service = services.find(s => s.id === id);
|
||||||
// Look up service in services.json for custom URLs
|
|
||||||
let service = null;
|
|
||||||
if (await exists(SERVICES_FILE)) {
|
|
||||||
const data = await servicesStateManager.read();
|
|
||||||
const services = Array.isArray(data) ? data : data.services || [];
|
|
||||||
service = services.find(s => s.id === id);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (service?.isExternal && service?.externalUrl) {
|
|
||||||
url = service.externalUrl;
|
|
||||||
} else if (service?.url) {
|
|
||||||
url = service.url.startsWith('http') ? service.url : `https://${service.url}`;
|
|
||||||
} else {
|
|
||||||
// Build URL from configured routing mode (subdomain or subdirectory)
|
|
||||||
url = buildServiceUrl(id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const url = resolveServiceUrl(id, service, siteConfig, buildServiceUrl);
|
||||||
|
|
||||||
const parsed = new URL(url);
|
const parsed = new URL(url);
|
||||||
const isHttps = parsed.protocol === 'https:';
|
const isHttps = parsed.protocol === 'https:';
|
||||||
const lib = isHttps ? https : http;
|
const lib = isHttps ? https : http;
|
||||||
@@ -1734,6 +1724,8 @@ async function addServiceToConfig(service) {
|
|||||||
return services;
|
return services;
|
||||||
});
|
});
|
||||||
log.info('deploy', 'Service added to config', { serviceId: service.id });
|
log.info('deploy', 'Service added to config', { serviceId: service.id });
|
||||||
|
// Sync health checker with updated services list
|
||||||
|
ctx.resyncHealthChecker?.().catch(() => {});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error('deploy', 'Failed to add service to config', { serviceId: service.id, error: error.message });
|
log.error('deploy', 'Failed to add service to config', { serviceId: service.id, error: error.message });
|
||||||
throw error;
|
throw error;
|
||||||
@@ -1852,7 +1844,7 @@ const server = app.listen(PORT, '0.0.0.0', () => {
|
|||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
// Auto-configure health checker from services.json
|
// Auto-configure health checker from services.json
|
||||||
await syncHealthCheckerServices({ log, SERVICES_FILE, servicesStateManager, healthChecker, buildDomain, APP });
|
await syncHealthCheckerServices({ log, SERVICES_FILE, servicesStateManager, healthChecker, buildServiceUrl, siteConfig, APP });
|
||||||
healthChecker.start();
|
healthChecker.start();
|
||||||
log.info('server', 'Health checker started');
|
log.info('server', 'Health checker started');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1887,37 +1879,41 @@ const server = app.listen(PORT, '0.0.0.0', () => {
|
|||||||
log.error('server', 'Self-updater failed to start', { error: error.message });
|
log.error('server', 'Self-updater failed to start', { error: error.message });
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
if (dockerMaintenance) {
|
||||||
dockerMaintenance.start();
|
try {
|
||||||
log.info('server', 'Docker maintenance started');
|
dockerMaintenance.start();
|
||||||
dockerMaintenance.on('maintenance-complete', (result) => {
|
log.info('server', 'Docker maintenance started');
|
||||||
const saved = Math.round(result.spaceReclaimed.total / 1024 / 1024);
|
dockerMaintenance.on('maintenance-complete', (result) => {
|
||||||
if (saved > 0 || result.warnings.length > 0) {
|
const saved = Math.round(result.spaceReclaimed.total / 1024 / 1024);
|
||||||
log.info('maintenance', 'Docker maintenance completed', {
|
if (saved > 0 || result.warnings.length > 0) {
|
||||||
spaceReclaimedMB: saved,
|
log.info('maintenance', 'Docker maintenance completed', {
|
||||||
pruned: result.pruned,
|
spaceReclaimedMB: saved,
|
||||||
warnings: result.warnings.length
|
pruned: result.pruned,
|
||||||
});
|
warnings: result.warnings.length
|
||||||
}
|
});
|
||||||
if (result.warnings.length > 0) {
|
}
|
||||||
for (const w of result.warnings) log.warn('maintenance', w);
|
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 (error) {
|
||||||
|
log.error('server', 'Docker maintenance failed to start', { error: error.message });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
if (logDigest) {
|
||||||
logDigest.start(platformPaths.digestDir);
|
try {
|
||||||
log.info('server', 'Log digest started', { digestDir: platformPaths.digestDir });
|
logDigest.start(platformPaths.digestDir);
|
||||||
logDigest.on('digest-generated', ({ date }) => {
|
log.info('server', 'Log digest started', { digestDir: platformPaths.digestDir });
|
||||||
log.info('digest', `Daily digest generated for ${date}`);
|
logDigest.on('digest-generated', ({ date }) => {
|
||||||
if (typeof ctx.notification?.send === 'function') {
|
log.info('digest', `Daily digest generated for ${date}`);
|
||||||
ctx.notification.send('system.digest', 'Daily Log Digest', `Log digest for ${date} is ready. View it in the DashCaddy dashboard.`, 'info');
|
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 });
|
} catch (error) {
|
||||||
|
log.error('server', 'Log digest failed to start', { error: error.message });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tailscale API sync (if OAuth configured)
|
// Tailscale API sync (if OAuth configured)
|
||||||
@@ -1935,8 +1931,8 @@ function shutdown(signal) {
|
|||||||
log.info('shutdown', `${signal} received, draining connections...`);
|
log.info('shutdown', `${signal} received, draining connections...`);
|
||||||
resourceMonitor.stop();
|
resourceMonitor.stop();
|
||||||
backupManager.stop();
|
backupManager.stop();
|
||||||
dockerMaintenance.stop();
|
if (dockerMaintenance) dockerMaintenance.stop();
|
||||||
logDigest.stop();
|
if (logDigest) logDigest.stop();
|
||||||
healthChecker.stop();
|
healthChecker.stop();
|
||||||
updateManager.stop();
|
updateManager.stop();
|
||||||
selfUpdater.stop();
|
selfUpdater.stop();
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const path = require('path');
|
|||||||
const http = require('http');
|
const http = require('http');
|
||||||
const https = require('https');
|
const https = require('https');
|
||||||
const { exists, isAccessible } = require('./fs-helpers');
|
const { exists, isAccessible } = require('./fs-helpers');
|
||||||
|
const { resolveServiceUrl } = require('./url-resolver');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate startup configuration and environment.
|
* Validate startup configuration and environment.
|
||||||
@@ -146,17 +147,19 @@ async function validateStartupConfig({ log, CADDYFILE_PATH, SERVICES_FILE, CONFI
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Auto-configure health checker from services.json + top-card services.
|
* Full-sync health checker from services.json + top-card services.
|
||||||
|
* Adds missing, updates changed URLs, removes deleted services.
|
||||||
*
|
*
|
||||||
* @param {Object} deps
|
* @param {Object} deps
|
||||||
* @param {Function} deps.log - structured logger
|
* @param {Function} deps.log - structured logger
|
||||||
* @param {string} deps.SERVICES_FILE
|
* @param {string} deps.SERVICES_FILE
|
||||||
* @param {Object} deps.servicesStateManager - StateManager instance
|
* @param {Object} deps.servicesStateManager - StateManager instance
|
||||||
* @param {Object} deps.healthChecker - health checker module
|
* @param {Object} deps.healthChecker - health checker module
|
||||||
* @param {Function} deps.buildDomain - builds domain from subdomain
|
* @param {Function} deps.buildServiceUrl - canonical URL builder
|
||||||
|
* @param {Object} deps.siteConfig - site config (dnsServers, etc.)
|
||||||
* @param {Object} deps.APP - app constants (USER_AGENTS)
|
* @param {Object} deps.APP - app constants (USER_AGENTS)
|
||||||
*/
|
*/
|
||||||
async function syncHealthCheckerServices({ log, SERVICES_FILE, servicesStateManager, healthChecker, buildDomain, APP }) {
|
async function syncHealthCheckerServices({ log, SERVICES_FILE, servicesStateManager, healthChecker, buildServiceUrl, siteConfig, APP }) {
|
||||||
try {
|
try {
|
||||||
const topCardServices = [
|
const topCardServices = [
|
||||||
{ id: 'dns1', name: 'DNS1' },
|
{ id: 'dns1', name: 'DNS1' },
|
||||||
@@ -164,6 +167,11 @@ async function syncHealthCheckerServices({ log, SERVICES_FILE, servicesStateMana
|
|||||||
{ id: 'internet', name: 'Internet' },
|
{ id: 'internet', name: 'Internet' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Add dns3 if it exists in dnsServers config
|
||||||
|
if (siteConfig?.dnsServers?.dns3) {
|
||||||
|
topCardServices.push({ id: 'dns3', name: 'DNS3' });
|
||||||
|
}
|
||||||
|
|
||||||
let appServices = [];
|
let appServices = [];
|
||||||
if (await exists(SERVICES_FILE)) {
|
if (await exists(SERVICES_FILE)) {
|
||||||
const data = await servicesStateManager.read();
|
const data = await servicesStateManager.read();
|
||||||
@@ -171,38 +179,47 @@ async function syncHealthCheckerServices({ log, SERVICES_FILE, servicesStateMana
|
|||||||
}
|
}
|
||||||
|
|
||||||
const allServices = [...topCardServices, ...appServices];
|
const allServices = [...topCardServices, ...appServices];
|
||||||
const configuredIds = new Set(Object.keys(healthChecker.config.services || {}));
|
const desiredIds = new Set();
|
||||||
let added = 0;
|
let added = 0, updated = 0, removed = 0;
|
||||||
|
|
||||||
for (const svc of allServices) {
|
for (const svc of allServices) {
|
||||||
const id = svc.id || svc.name?.toLowerCase();
|
const id = svc.id || svc.name?.toLowerCase();
|
||||||
if (!id || configuredIds.has(id)) continue;
|
if (!id) continue;
|
||||||
|
desiredIds.add(id);
|
||||||
|
|
||||||
let url;
|
const url = resolveServiceUrl(id, svc, siteConfig, buildServiceUrl);
|
||||||
if (id === 'internet') {
|
const existing = healthChecker.config.services?.[id];
|
||||||
url = 'https://www.google.com';
|
|
||||||
} else if (svc.isExternal && svc.externalUrl) {
|
if (!existing) {
|
||||||
url = svc.externalUrl;
|
healthChecker.configureService(id, {
|
||||||
} else {
|
name: svc.name || id,
|
||||||
url = `https://${buildDomain(id)}`;
|
url,
|
||||||
|
method: 'HEAD',
|
||||||
|
timeout: 5000,
|
||||||
|
expectedStatusCodes: [200, 201, 204, 301, 302, 303, 307, 308, 401, 403],
|
||||||
|
headers: { 'User-Agent': APP.USER_AGENTS.HEALTH },
|
||||||
|
});
|
||||||
|
added++;
|
||||||
|
} else if (existing.url !== url) {
|
||||||
|
healthChecker.configureService(id, { ...existing, url });
|
||||||
|
updated++;
|
||||||
}
|
}
|
||||||
|
|
||||||
healthChecker.configureService(id, {
|
|
||||||
name: svc.name || id,
|
|
||||||
url,
|
|
||||||
method: 'HEAD',
|
|
||||||
timeout: 5000,
|
|
||||||
expectedStatusCodes: [200, 201, 204, 301, 302, 303, 307, 308, 401, 403],
|
|
||||||
headers: { 'User-Agent': APP.USER_AGENTS.HEALTH },
|
|
||||||
});
|
|
||||||
added++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (added > 0) {
|
// Remove services no longer in the desired set
|
||||||
log.info('health', 'Auto-configured services from services.json', { count: added });
|
const configuredIds = Object.keys(healthChecker.config.services || {});
|
||||||
|
for (const id of configuredIds) {
|
||||||
|
if (!desiredIds.has(id)) {
|
||||||
|
healthChecker.removeService(id);
|
||||||
|
removed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (added > 0 || updated > 0 || removed > 0) {
|
||||||
|
log.info('health', 'Health checker synced', { added, updated, removed });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error('health', 'Error syncing services', { error: error.message });
|
log.error('health', 'Error syncing health checker', { error: error.message });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
32
dashcaddy-api/url-resolver.js
Normal file
32
dashcaddy-api/url-resolver.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
/**
|
||||||
|
* Unified URL Resolver
|
||||||
|
* Single source of truth for resolving service URLs across all systems
|
||||||
|
* (probes, health checks, health checker auto-config, SSO).
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve the canonical URL for a service.
|
||||||
|
*
|
||||||
|
* Priority:
|
||||||
|
* 1. internet → https://www.google.com
|
||||||
|
* 2. isExternal + externalUrl → use as-is
|
||||||
|
* 3. service.url → prepend https:// if no protocol
|
||||||
|
* 4. dnsServers config → http://{ip}:{port}
|
||||||
|
* 5. fallback → buildServiceUrl(id)
|
||||||
|
*
|
||||||
|
* @param {string} id - service identifier
|
||||||
|
* @param {Object|null} service - service object from services.json (may be null for top-card services)
|
||||||
|
* @param {Object|null} siteConfig - site config containing dnsServers etc.
|
||||||
|
* @param {Function} buildServiceUrl - fallback URL builder (subdomain or subdirectory mode)
|
||||||
|
* @returns {string} resolved URL
|
||||||
|
*/
|
||||||
|
function resolveServiceUrl(id, service, siteConfig, buildServiceUrl) {
|
||||||
|
if (id === 'internet') return 'https://www.google.com';
|
||||||
|
if (service?.isExternal && service.externalUrl) return service.externalUrl;
|
||||||
|
if (service?.url) return service.url.startsWith('http') ? service.url : `https://${service.url}`;
|
||||||
|
const dnsServer = siteConfig?.dnsServers?.[id];
|
||||||
|
if (dnsServer) return `http://${dnsServer.ip}:${dnsServer.port || 5380}`;
|
||||||
|
return buildServiceUrl(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { resolveServiceUrl };
|
||||||
Reference in New Issue
Block a user