diff --git a/CHANGELOG.md b/CHANGELOG.md index 36cd82e6..70e87013 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- Apps with a container check no longer show an unhealthy badge while Docker + container health checks are off. + +- An app's health check can be turned off while Docker container health checks + are off. + - A widget's settings now open from that widget's own saved config. - A tile with only a fixed label set now shows that badge. diff --git a/api/src/routes/health.js b/api/src/routes/health.js index 7821edb9..820236c4 100644 --- a/api/src/routes/health.js +++ b/api/src/routes/health.js @@ -20,10 +20,11 @@ const demoData = require('../demo-data'); const log = require('../log'); const SOCKET_PROXY_URL_DEFAULT = process.env.SOCKET_PROXY_URL || ''; -async function fetchContainerHealth() { - const cfg = loadConfig(); - const socketUrl = cfg.settings?.server?.socketProxyUrl || SOCKET_PROXY_URL_DEFAULT; - if (!socketUrl) return Object.create(null); +function socketProxyUrl(cfg) { + return cfg.settings?.server?.socketProxyUrl || SOCKET_PROXY_URL_DEFAULT; +} + +async function fetchContainerHealth(socketUrl) { try { const r = await fetchUnchecked(`${socketUrl}/containers/json?all=true`); if (!Array.isArray(r.data)) return Object.create(null); @@ -133,15 +134,18 @@ on('GET', '/api/health', async (req, res) => { const cfg = loadConfig(); return json(res, 200, demoData.demoHealth(cfg.items)); } - const containers = await fetchContainerHealth(); - const cfg = loadConfig(), - result = Object.create(null); + const cfg = loadConfig(); + const socketUrl = socketProxyUrl(cfg); + /* Without a proxy address every container lookup misses, which would report + every container-backed app as unhealthy. */ + const containers = socketUrl ? await fetchContainerHealth(socketUrl) : Object.create(null); + const result = Object.create(null); await Promise.allSettled( cfg.items .filter(i => i.type === 'app' && (i.container || i.ping || i.monitoring?.healthcheck?.enabled)) .map(async item => { const mon = item.monitoring?.healthcheck || {}; - const cName = mon.container || item.container || ''; + const cName = socketUrl ? mon.container || item.container || '' : ''; const ping = mon.pingUrl || item.ping || ''; /* Build the entry up rather than replace it. An item can have both a container and a ping, and the tile needs the detail from each. */ @@ -170,7 +174,7 @@ on('GET', '/api/health', async (req, res) => { detail.pingStatus = r.status; detail.pingError = r.error; } - result[item.id] = { unhealthy, ...detail }; + if (cName || ping) result[item.id] = { unhealthy, ...detail }; }), ); json(res, 200, result); diff --git a/api/test/health-route.test.js b/api/test/health-route.test.js index ee25ad1b..aba2b8e7 100644 --- a/api/test/health-route.test.js +++ b/api/test/health-route.test.js @@ -2,6 +2,7 @@ const path = require('node:path'); const { tmpDir } = require('../test-support/tmp'); process.env.CONFIG_PATH = path.join(tmpDir('health'), 'apps.json'); +delete process.env.SOCKET_PROXY_URL; const { test, before, after } = require('node:test'); const assert = require('node:assert/strict'); @@ -62,10 +63,10 @@ function health() { }); } -function configure(item) { +function configure(item, socketProxyUrl = socketBase) { saveConfig({ items: [Object.assign({ id: 'a1', type: 'app', name: 'App' }, item)], - settings: { server: { socketProxyUrl: socketBase } }, + settings: { server: { socketProxyUrl } }, }); } @@ -157,3 +158,19 @@ test('a container actually named __proto__ is matched, not discarded', async () assert.equal(r.unhealthy, true); assert.equal(r.state, 'exited'); }); + +test('a container check is skipped while no socket proxy is configured', async () => { + containers = []; + configure({ container: 'myapp', monitoring: { healthcheck: { enabled: true, container: 'myapp' } } }, ''); + assert.equal((await health()).a1, undefined); +}); + +test('a ping check still runs while no socket proxy is configured', async () => { + containers = []; + targetStatus = 200; + configure({ container: 'myapp', ping: `${targetBase}/` }, ''); + const r = (await health()).a1; + assert.equal(r.unhealthy, false); + assert.equal(r.pingStatus, 200); + assert.equal(r.state, undefined); +}); diff --git a/ui/admin/index.html b/ui/admin/index.html index a5b14afe..e83f5eee 100644 --- a/ui/admin/index.html +++ b/ui/admin/index.html @@ -303,6 +303,6 @@

General

- + diff --git a/ui/i18n/de.json b/ui/i18n/de.json index d9348935..524869a5 100644 --- a/ui/i18n/de.json +++ b/ui/i18n/de.json @@ -136,7 +136,7 @@ "dockFull": "Dock voll ({max}/{max}). Entferne zuerst eine App.", "badge": "Badge", "healthCheck": "Zustandsprüfung", - "healthGlobalOff": "Aktiviere Docker-Container-Zustandsprüfungen unter Allgemein und richte sie ein, um dies zu verwenden.", + "healthGlobalOff": "Container-Prüfungen brauchen Docker-Container-Zustandsprüfungen unter Allgemein. URL-Prüfungen funktionieren auch ohne.", "type": "Typ", "container": "Container", "ping": "Ping", diff --git a/ui/i18n/en.json b/ui/i18n/en.json index fbce8062..3e97e8ac 100644 --- a/ui/i18n/en.json +++ b/ui/i18n/en.json @@ -136,7 +136,7 @@ "dockFull": "Dock full ({max}/{max}). Remove an app first.", "badge": "Badge", "healthCheck": "Health Check", - "healthGlobalOff": "Turn on Docker Container Health Checks in General, then configure it, to use this.", + "healthGlobalOff": "Container checks need Docker Container Health Checks turned on in General. URL checks work without it.", "type": "Type", "container": "Container", "ping": "Ping", diff --git a/ui/i18n/es.json b/ui/i18n/es.json index 8e65b4fb..fe763f52 100644 --- a/ui/i18n/es.json +++ b/ui/i18n/es.json @@ -136,7 +136,7 @@ "dockFull": "Dock lleno ({max}/{max}). Elimina una aplicación primero.", "badge": "Insignia", "healthCheck": "Comprobación de estado", - "healthGlobalOff": "Activa las Comprobaciones de estado de contenedores Docker en General y configúralas para usar esto.", + "healthGlobalOff": "Las comprobaciones de contenedor requieren las Comprobaciones de estado de contenedores Docker en General. Las comprobaciones de URL funcionan sin ellas.", "type": "Tipo", "container": "Contenedor", "ping": "Ping", diff --git a/ui/i18n/fa.json b/ui/i18n/fa.json index 2fbc006c..a393d9e2 100644 --- a/ui/i18n/fa.json +++ b/ui/i18n/fa.json @@ -136,7 +136,7 @@ "dockFull": "Dock پر است ({max}/{max}). ابتدا یک برنامه را حذف کنید.", "badge": "نشان", "healthCheck": "بررسی سلامت", - "healthGlobalOff": "برای استفاده از این، بررسی سلامت کانتینرهای داکر را در عمومی روشن و پیکربندی کنید.", + "healthGlobalOff": "بررسی کانتینر به روشن‌بودن بررسی سلامت کانتینرهای داکر در عمومی نیاز دارد. بررسی نشانی اینترنتی بدون آن هم کار می‌کند.", "type": "نوع", "container": "کانتینر", "ping": "پینگ", diff --git a/ui/i18n/fr.json b/ui/i18n/fr.json index c430d8c5..b0ca05b7 100644 --- a/ui/i18n/fr.json +++ b/ui/i18n/fr.json @@ -136,7 +136,7 @@ "dockFull": "Dock plein ({max}/{max}). Retirez d'abord une application.", "badge": "Badge", "healthCheck": "Vérification de l'état", - "healthGlobalOff": "Activez les Vérifications de l'état des conteneurs Docker dans Général, puis configurez-les, pour utiliser ceci.", + "healthGlobalOff": "Les vérifications de conteneur exigent les Vérifications de l'état des conteneurs Docker dans Général. Les vérifications d'URL fonctionnent sans.", "type": "Type", "container": "Conteneur", "ping": "Ping", diff --git a/ui/i18n/zh-Hans.json b/ui/i18n/zh-Hans.json index 02826581..8334cc1e 100644 --- a/ui/i18n/zh-Hans.json +++ b/ui/i18n/zh-Hans.json @@ -136,7 +136,7 @@ "dockFull": "Dock 已满 ({max}/{max})。请先移除一个应用。", "badge": "徽章", "healthCheck": "健康检查", - "healthGlobalOff": "请先在通用中开启 Docker 容器健康检查并完成配置后再使用。", + "healthGlobalOff": "容器检查需要在通用中开启 Docker 容器健康检查。网址检查无需开启即可使用。", "type": "类型", "container": "容器", "ping": "Ping", diff --git a/ui/js/admin-app-form.js b/ui/js/admin-app-form.js index 090de2bb..9dccbb3e 100644 --- a/ui/js/admin-app-form.js +++ b/ui/js/admin-app-form.js @@ -138,7 +138,7 @@ export function buildAppForm(body, item) {

${t('app.badge')}

${t('app.healthCheck')}${tog('hc-en', hc.enabled)}
-
+
${t('app.type')}
@@ -238,13 +238,12 @@ export function buildAppForm(body, item) { setTogDisabled(el('f-dock'), dockBlocked, 'dock-full-tip'); const hcEn = el('hc-en'); - setTogDisabled(hcEn, !globalHealthOn, 'hc-off-tip'); const showHide = (id, on) => { const node = el(id); if (node) node.hidden = !on; }; hcEn?.addEventListener('change', e => { - if (globalHealthOn) showHide('hc-sub', tgt(e).checked); + showHide('hc-sub', tgt(e).checked); }); document.querySelectorAll('input[name="hc-type"]').forEach(r => r.addEventListener('change', () => { diff --git a/ui/js/admin.js b/ui/js/admin.js index 00ab543a..92aadfa9 100644 --- a/ui/js/admin.js +++ b/ui/js/admin.js @@ -1,4 +1,4 @@ -import { buildAppForm, buildFolderForm, serializeKvRows } from '/js/admin-app-form.js?v=f8fda76d'; +import { buildAppForm, buildFolderForm, serializeKvRows } from '/js/admin-app-form.js?v=848f1e7d'; import { checkAuth, requireLogin, wirePasswordStrength } from '/js/admin-auth.js?v=76be763e'; import { applyDrop, canJoinFolder } from '/js/admin-drag-logic.js?v=8fcf583a'; import { reorderItems, resolveAdminSection } from '/js/admin-logic.js?v=f3f87abf';