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:
2026-03-14 23:01:20 -07:00
parent 70b818c2bd
commit 2815233e86
7 changed files with 145 additions and 106 deletions

View File

@@ -10,6 +10,7 @@ const path = require('path');
const http = require('http');
const https = require('https');
const { exists, isAccessible } = require('./fs-helpers');
const { resolveServiceUrl } = require('./url-resolver');
/**
* 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 {Function} deps.log - structured logger
* @param {string} deps.SERVICES_FILE
* @param {Object} deps.servicesStateManager - StateManager instance
* @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)
*/
async function syncHealthCheckerServices({ log, SERVICES_FILE, servicesStateManager, healthChecker, buildDomain, APP }) {
async function syncHealthCheckerServices({ log, SERVICES_FILE, servicesStateManager, healthChecker, buildServiceUrl, siteConfig, APP }) {
try {
const topCardServices = [
{ id: 'dns1', name: 'DNS1' },
@@ -164,6 +167,11 @@ async function syncHealthCheckerServices({ log, SERVICES_FILE, servicesStateMana
{ id: 'internet', name: 'Internet' },
];
// Add dns3 if it exists in dnsServers config
if (siteConfig?.dnsServers?.dns3) {
topCardServices.push({ id: 'dns3', name: 'DNS3' });
}
let appServices = [];
if (await exists(SERVICES_FILE)) {
const data = await servicesStateManager.read();
@@ -171,38 +179,47 @@ async function syncHealthCheckerServices({ log, SERVICES_FILE, servicesStateMana
}
const allServices = [...topCardServices, ...appServices];
const configuredIds = new Set(Object.keys(healthChecker.config.services || {}));
let added = 0;
const desiredIds = new Set();
let added = 0, updated = 0, removed = 0;
for (const svc of allServices) {
const id = svc.id || svc.name?.toLowerCase();
if (!id || configuredIds.has(id)) continue;
if (!id) continue;
desiredIds.add(id);
let url;
if (id === 'internet') {
url = 'https://www.google.com';
} else if (svc.isExternal && svc.externalUrl) {
url = svc.externalUrl;
} else {
url = `https://${buildDomain(id)}`;
const url = resolveServiceUrl(id, svc, siteConfig, buildServiceUrl);
const existing = healthChecker.config.services?.[id];
if (!existing) {
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++;
} 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) {
log.info('health', 'Auto-configured services from services.json', { count: added });
// Remove services no longer in the desired set
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) {
log.error('health', 'Error syncing services', { error: error.message });
log.error('health', 'Error syncing health checker', { error: error.message });
}
}