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,
|
||||
safeErrorMessage: null,
|
||||
buildDomain: null,
|
||||
buildServiceUrl: null,
|
||||
getServiceById: null,
|
||||
readConfig: null,
|
||||
saveConfig: null,
|
||||
addServiceToConfig: null,
|
||||
resyncHealthChecker: null,
|
||||
validateURL: null,
|
||||
|
||||
// Middleware
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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'));
|
||||
|
||||
@@ -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}`,
|
||||
|
||||
Reference in New Issue
Block a user