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

@@ -111,10 +111,12 @@ const ctx = {
logError: null,
safeErrorMessage: null,
buildDomain: null,
buildServiceUrl: null,
getServiceById: null,
readConfig: null,
saveConfig: null,
addServiceToConfig: null,
resyncHealthChecker: null,
validateURL: null,
// Middleware

View File

@@ -6,6 +6,7 @@ const { TIMEOUTS } = require('../constants');
const { exists } = require('../fs-helpers');
const { paginate, parsePaginationParams } = require('../pagination');
const platformPaths = require('../platform-paths');
const { resolveServiceUrl } = require('../url-resolver');
module.exports = function(ctx) {
const router = express.Router();
@@ -36,18 +37,7 @@ module.exports = function(ctx) {
let checkType = 'http';
// Determine URL to check
if (service.isExternal && service.externalUrl) {
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)}`;
}
url = resolveServiceUrl(serviceId, service, ctx.siteConfig, ctx.buildServiceUrl);
if (!url) {
health[serviceId] = { status: 'unknown', reason: 'No URL configured' };
@@ -157,14 +147,7 @@ module.exports = function(ctx) {
}
// Determine URL
let url = null;
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)}`;
}
const url = resolveServiceUrl(serviceId, service, ctx.siteConfig, ctx.buildServiceUrl);
// Check health
const controller = new AbortController();

View File

@@ -140,6 +140,7 @@ module.exports = function(ctx) {
// Get latest daily digest
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();
if (!digest) {
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)
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();
res.json({ success: true, ...live });
}, 'logs-digest-live'));
// List available digest dates
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();
res.json({ success: true, dates });
}, 'logs-digest-history'));
// Generate digest on demand (for today or a specific date)
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 digest = await ctx.logDigest.generateDailyDigest(date);
res.json({ success: true, digest });
@@ -168,6 +172,7 @@ module.exports = function(ctx) {
// Get digest for a specific date (JSON)
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;
if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) {
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
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 status = ctx.dockerMaintenance.getStatus();
res.json({ success: true, diskUsage, maintenance: status });
@@ -193,6 +199,7 @@ module.exports = function(ctx) {
// Trigger Docker maintenance manually
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();
res.json({ success: true, result });
}, 'logs-docker-maintenance'));

View File

@@ -8,6 +8,7 @@ const { APP, REGEX, TIMEOUTS } = require('../constants');
const { validateServiceConfig, isValidPort } = require('../input-validator');
const { exists } = require('../fs-helpers');
const { paginate, parsePaginationParams } = require('../pagination');
const { resolveServiceUrl } = require('../url-resolver');
module.exports = function(ctx) {
const router = express.Router();
@@ -33,10 +34,7 @@ module.exports = function(ctx) {
}
function resolveProbeUrl(id, service) {
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}`;
return ctx.buildServiceUrl(id);
return resolveServiceUrl(id, service, ctx.siteConfig, ctx.buildServiceUrl);
}
function requestStatusCode(url, method) {
@@ -308,6 +306,7 @@ module.exports = function(ctx) {
return services;
});
ctx.resyncHealthChecker?.().catch(() => {});
res.json({ success: true, message: `Service "${name}" added to dashboard` });
} catch (error) {
ctx.log.error('deploy', 'Error adding service', { error: error.message });
@@ -339,6 +338,7 @@ module.exports = function(ctx) {
}
await ctx.servicesStateManager.write(services);
ctx.resyncHealthChecker?.().catch(() => {});
res.json({
success: true,
@@ -367,6 +367,7 @@ module.exports = function(ctx) {
return ctx.errorResponse(res, 404, `Service "${id}" not found`);
}
ctx.resyncHealthChecker?.().catch(() => {});
res.json({ success: true, message: `Service "${id}" removed from dashboard` });
}, 'services-delete'));
@@ -454,6 +455,7 @@ module.exports = function(ctx) {
});
}
ctx.resyncHealthChecker?.().catch(() => {});
res.json({
success: true,
message: `Service updated: ${oldSubdomain} -> ${newSubdomain}`,