From 1f0f41d7131847ab90b9531307bae24c7160212e Mon Sep 17 00:00:00 2001 From: mstrhakr Date: Tue, 1 Sep 2026 12:54:32 -0400 Subject: [PATCH 1/2] fix(dashboard): load icon helpers on Dashboard tile (#143) composeIconSrc/composeIconFallback lived in composeManagerMain.js, which is only loaded on the Compose/Docker tab. On the Dashboard the stack render loop threw a ReferenceError, so the tile stayed on 'Loading...' and compose containers were never hidden from the Docker tile. Move the icon helpers into composeIcons.js and load it from both the Compose page and the dashboard tile. Also run container hiding before the render loop so a render failure can no longer disable it. --- .../compose.manager.dashboard.page | 18 +++--- .../include/ComposeManager.php | 1 + .../javascript/composeIcons.js | 58 +++++++++++++++++++ .../javascript/composeManagerMain.js | 47 +-------------- 4 files changed, 72 insertions(+), 52 deletions(-) create mode 100644 source/compose.manager/javascript/composeIcons.js diff --git a/source/compose.manager/compose.manager.dashboard.page b/source/compose.manager/compose.manager.dashboard.page index f847c9d..f6cf484 100644 --- a/source/compose.manager/compose.manager.dashboard.page +++ b/source/compose.manager/compose.manager.dashboard.page @@ -80,8 +80,13 @@ EOT; // Debug setting from config $debugEnabled = isset($cfg['DEBUG_TO_LOG']) && $cfg['DEBUG_TO_LOG'] === 'true' ? 'true' : 'false'; $hideComposeContainersJs = $hideDockerComposeContainers ? 'true' : 'false'; +// Icon helpers live in their own file because composeManagerMain.js is not loaded on the Dashboard +$iconsJsSrc = '/plugins/compose.manager/javascript/composeIcons.js'; +$iconsJsVer = @filemtime('/usr/local/emhttp' . $iconsJsSrc); +if ($iconsJsVer) $iconsJsSrc .= '?v=' . $iconsJsVer; // CSS and JavaScript $configScript = << + diff --git a/source/compose.manager/javascript/composeIcons.js b/source/compose.manager/javascript/composeIcons.js new file mode 100644 index 0000000..399ca53 --- /dev/null +++ b/source/compose.manager/javascript/composeIcons.js @@ -0,0 +1,58 @@ +/** + * Shared icon helpers. + * Loaded by both the Compose page and the Dashboard tile — the tile has no + * access to composeManagerMain.js, so these must live in their own file. + */ + +function composeIconFallback(img) { + if (!img || img.dataset.composeFallbackApplied === 'true') { + return; + } + img.dataset.composeFallbackApplied = 'true'; + img.onerror = null; + img.src = '/plugins/compose.manager/images/question.png'; +} + +// Validate an icon source: http(s) URL, data URI, or local server path +function isValidIconSrc(src) { + if (!src) return false; + var s = src.trim(); + return s.indexOf('http://') === 0 || s.indexOf('https://') === 0 || + s.indexOf('data:image/') === 0 || s.indexOf('/') === 0; +} + +function isCacheEligibleLocalIconPath(src) { + if (!src) return false; + var s = src.trim(); + return s.indexOf('/mnt/') === 0 || s.indexOf('/boot/config/plugins/compose.manager/') === 0; +} + +/** Route cache-eligible icons through the local cache proxy; passthrough otherwise. */ +function composeIconSrc(src, containerName) { + if (!src || !isValidIconSrc(src)) { + return '/plugins/compose.manager/images/question.png'; + } + var s = src.trim(); + if (s.indexOf('/plugins/compose.manager/IconCache.php?') === 0) { + return s; + } + + var cacheable = s.indexOf('http://') === 0 || s.indexOf('https://') === 0 || + s.indexOf('data:image/') === 0 || isCacheEligibleLocalIconPath(s); + if (cacheable) { + var proxied = '/plugins/compose.manager/IconCache.php?src=' + encodeURIComponent(s); + if (containerName && /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(containerName)) { + proxied += '&ct=' + encodeURIComponent(containerName); + } + return proxied; + } + + return s; +} + +if (typeof window !== 'undefined') { + window.composeIconFallback = composeIconFallback; + window.isValidIconSrc = isValidIconSrc; + window.isCacheEligibleLocalIconPath = isCacheEligibleLocalIconPath; + window.composeIconSrc = composeIconSrc; +} diff --git a/source/compose.manager/javascript/composeManagerMain.js b/source/compose.manager/javascript/composeManagerMain.js index 1fa5357..5a21464 100644 --- a/source/compose.manager/javascript/composeManagerMain.js +++ b/source/compose.manager/javascript/composeManagerMain.js @@ -2452,51 +2452,8 @@ function isValidWebUIUrl(url) { } } -function composeIconFallback(img) { - if (!img || img.dataset.composeFallbackApplied === 'true') { - return; - } - img.dataset.composeFallbackApplied = 'true'; - img.onerror = null; - img.src = '/plugins/compose.manager/images/question.png'; -} - -// Validate an icon source: http(s) URL, data URI, or local server path -function isValidIconSrc(src) { - if (!src) return false; - var s = src.trim(); - return s.indexOf('http://') === 0 || s.indexOf('https://') === 0 || - s.indexOf('data:image/') === 0 || s.indexOf('/') === 0; -} - -function isCacheEligibleLocalIconPath(src) { - if (!src) return false; - var s = src.trim(); - return s.indexOf('/mnt/') === 0 || s.indexOf('/boot/config/plugins/compose.manager/') === 0; -} - -/** Route cache-eligible icons through the local cache proxy; passthrough otherwise. */ -function composeIconSrc(src, containerName) { - if (!src || !isValidIconSrc(src)) { - return '/plugins/compose.manager/images/question.png'; - } - var s = src.trim(); - if (s.indexOf('/plugins/compose.manager/IconCache.php?') === 0) { - return s; - } - - var cacheable = s.indexOf('http://') === 0 || s.indexOf('https://') === 0 || - s.indexOf('data:image/') === 0 || isCacheEligibleLocalIconPath(s); - if (cacheable) { - var proxied = '/plugins/compose.manager/IconCache.php?src=' + encodeURIComponent(s); - if (containerName && /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(containerName)) { - proxied += '&ct=' + encodeURIComponent(containerName); - } - return proxied; - } - - return s; -} +// composeIconFallback/isValidIconSrc/isCacheEligibleLocalIconPath/composeIconSrc +// live in composeIcons.js (shared with the dashboard tile). // Sanitize user-entered icon values before assigning to image src in live preview. function sanitizeIconPreviewSrc(raw) { From 42a4ab552ca29598b4d9a36bacf5c82391b9509b Mon Sep 17 00:00:00 2001 From: mstrhakr Date: Tue, 1 Sep 2026 13:04:35 -0400 Subject: [PATCH 2/2] fix(icon): pass data URIs straight to the browser instead of proxying IconCache.php?src= puts the whole base64 payload in the query string, which trips browser/nginx URL length limits and breaks the icon. Data URIs are already inline, so proxying gains nothing; Docker Manager seeding for them still happens server-side in Exec.php. --- source/compose.manager/include/Util.php | 8 +++++--- source/compose.manager/javascript/composeIcons.js | 7 +++++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/source/compose.manager/include/Util.php b/source/compose.manager/include/Util.php index ff91366..2aacac8 100644 --- a/source/compose.manager/include/Util.php +++ b/source/compose.manager/include/Util.php @@ -157,7 +157,10 @@ function compose_icon_ext_to_mime(string $ext): string } if (!function_exists('compose_icon_browser_url')) { - /** Return the URL the browser should use: proxy for http(s), passthrough otherwise. */ + /** + * Return the URL the browser should use: proxy for http(s), passthrough otherwise. + * Data URIs are never proxied — the base64 payload would blow past URL length limits. + */ function compose_icon_browser_url(string $src): string { $src = trim($src); @@ -172,10 +175,9 @@ function compose_icon_browser_url(string $src): string } $isRemote = strncasecmp($src, 'http://', 7) === 0 || strncasecmp($src, 'https://', 8) === 0; - $isData = strncasecmp($src, 'data:image/', 11) === 0; $isCacheableLocal = str_starts_with($src, '/mnt/') || str_starts_with($src, '/boot/config/plugins/compose.manager/'); - if ($isRemote || $isData || $isCacheableLocal) { + if ($isRemote || $isCacheableLocal) { return '/plugins/compose.manager/IconCache.php?src=' . urlencode($src); } diff --git a/source/compose.manager/javascript/composeIcons.js b/source/compose.manager/javascript/composeIcons.js index 399ca53..52ac0fa 100644 --- a/source/compose.manager/javascript/composeIcons.js +++ b/source/compose.manager/javascript/composeIcons.js @@ -27,7 +27,10 @@ function isCacheEligibleLocalIconPath(src) { return s.indexOf('/mnt/') === 0 || s.indexOf('/boot/config/plugins/compose.manager/') === 0; } -/** Route cache-eligible icons through the local cache proxy; passthrough otherwise. */ +/** + * Route cache-eligible icons through the local cache proxy; passthrough otherwise. + * Data URIs are never proxied — the base64 payload would blow past URL length limits. + */ function composeIconSrc(src, containerName) { if (!src || !isValidIconSrc(src)) { return '/plugins/compose.manager/images/question.png'; @@ -38,7 +41,7 @@ function composeIconSrc(src, containerName) { } var cacheable = s.indexOf('http://') === 0 || s.indexOf('https://') === 0 || - s.indexOf('data:image/') === 0 || isCacheEligibleLocalIconPath(s); + isCacheEligibleLocalIconPath(s); if (cacheable) { var proxied = '/plugins/compose.manager/IconCache.php?src=' + encodeURIComponent(s); if (containerName && /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(containerName)) {