Skip to content

Commit 92dc336

Browse files
Fix unresolved Docker WebUI targets
1 parent 5c4743e commit 92dc336

6 files changed

Lines changed: 55 additions & 11 deletions

File tree

14.2 MB
Binary file not shown.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
223e8bbe32d68df73289b93c9b6848793e595b2372225e8eaa9ca1f68a889c39 folderview.plus-2026.03.29.06.txz

folderview.plus.plg

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,18 @@
66
<!ENTITY launch "Settings/FolderViewPlus">
77
<!ENTITY plugdir "/usr/local/emhttp/plugins/&name;">
88
<!ENTITY pluginURL "https://raw.githubusercontent.com/&github;/dev/folderview.plus.plg">
9-
<!ENTITY version "2026.03.29.05">
10-
<!ENTITY md5 "5baaaec1191be407fc79f10975cbc3fb">
9+
<!ENTITY version "2026.03.29.06">
10+
<!ENTITY md5 "fbcbf84f31aa881f80a8d6e684f0b075">
1111
]>
1212

1313
<PLUGIN name="&name;" author="&author;" version="&version;" launch="&launch;" pluginURL="&pluginURL;" icon="folder-icon.png" support="https://forums.unraid.net/topic/197631-plugin-folderview-plus/" min="7.0.0">
1414
<CHANGES>
1515

16+
###2026.03.29.06
17+
- Fix: Stop Docker WebUI buttons and Open all WebUIs from using unresolved template URLs like http://[IP]:[PORT:80] during the lightweight runtime pass.
18+
- Maintenance: Rebuild folder runtime WebUI targets after full Docker detail hydration so resolved ports replace stale placeholder values.
19+
20+
1621
###2026.03.29.05
1722
- Fix: Restore Docker WebUI launches for preview icons, tooltip actions, and Open all WebUIs without landing on about:blank#blocked.
1823
- Maintenance: Keep release-guard-compatible target=_blank markup while routing WebUI clicks through the safe launcher path.

src/folderview.plus/usr/local/emhttp/plugins/folderview.plus/scripts/docker.js

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -481,10 +481,16 @@ const buildDockerRuntimeInfoRenderEntry = (name, entry = {}, previousEntry = nul
481481
const previousInfo = previous?.info && typeof previous.info === 'object' ? previous.info : {};
482482
const previousState = previousInfo.State && typeof previousInfo.State === 'object' ? previousInfo.State : {};
483483
const previousConfig = previousInfo.Config && typeof previousInfo.Config === 'object' ? previousInfo.Config : {};
484+
const sourceInfo = source?.info && typeof source.info === 'object' ? source.info : {};
485+
const sourceState = sourceInfo.State && typeof sourceInfo.State === 'object' ? sourceInfo.State : {};
484486
const stateKind = String(source.state || '').trim().toLowerCase();
485487
const running = source.running === true || stateKind === 'running';
486488
const paused = source.paused === true || stateKind === 'paused';
487489
const manager = String(source.manager || previousState.manager || '').trim();
490+
const labelWebUi = String(labels['net.unraid.docker.webui'] || '').trim();
491+
const labelTsWebUi = String(labels['net.unraid.docker.tailscale.webui'] || '').trim();
492+
const resolvedWebUi = resolvePreferredWebuiValue(sourceState.WebUi, source.WebUi, source.webui, previousState.WebUi, labelWebUi);
493+
const resolvedTsWebUi = resolvePreferredWebuiValue(sourceState.TSWebUi, source.TSWebUi, previousState.TSWebUi, labelTsWebUi);
488494
const nextEntry = previous ? { ...previous } : {};
489495
nextEntry.shortId = String(source.id || previous?.shortId || '').trim();
490496
nextEntry.shortImageId = String(source.shortImageId || previous?.shortImageId || '').trim();
@@ -507,8 +513,8 @@ const buildDockerRuntimeInfoRenderEntry = (name, entry = {}, previousEntry = nul
507513
Autostart: source.autostart === true,
508514
Updated: (typeof previousState.Updated === 'boolean') ? previousState.Updated : null,
509515
manager,
510-
WebUi: String(labels['net.unraid.docker.webui'] || previousState.WebUi || '').trim(),
511-
TSWebUi: String(labels['net.unraid.docker.tailscale.webui'] || previousState.TSWebUi || '').trim()
516+
WebUi: resolvedWebUi,
517+
TSWebUi: resolvedTsWebUi
512518
},
513519
Ports: Array.isArray(previousInfo.Ports) ? previousInfo.Ports : [],
514520
template: previousInfo.template || null
@@ -547,9 +553,21 @@ const sanitizeImageSrc = typeof utils.sanitizeImageSrc === 'function'
547553
});
548554
const WEBUI_LINK_REL = 'noopener noreferrer';
549555
const WEBUI_OPEN_REL = 'noopener';
556+
const WEBUI_TEMPLATE_TOKEN_REGEX = /\[(?:IP|PORT:[^\]]+|HOSTNAME|MAGICDNS|NOSERVE)\]/i;
557+
const hasUnresolvedWebuiTemplateTokens = (value) => WEBUI_TEMPLATE_TOKEN_REGEX.test(String(value || '').trim());
558+
const resolvePreferredWebuiValue = (...candidates) => {
559+
for (const candidate of candidates) {
560+
const raw = String(candidate || '').trim();
561+
if (!raw || /^javascript:/i.test(raw) || hasUnresolvedWebuiTemplateTokens(raw)) {
562+
continue;
563+
}
564+
return raw;
565+
}
566+
return '';
567+
};
550568
const getSafeWebuiUrl = (value) => {
551569
const raw = String(value || '').trim();
552-
return raw && !/^javascript:/i.test(raw) ? raw : '';
570+
return raw && !/^javascript:/i.test(raw) && !hasUnresolvedWebuiTemplateTokens(raw) ? raw : '';
553571
};
554572
const openWebuiInNewTab = (url) => {
555573
const safeUrl = getSafeWebuiUrl(url);
@@ -2083,7 +2101,7 @@ const buildRuntimeContainerEntry = (name, sourceMeta = null) => {
20832101
id: source.id || String(runtime?.shortId || '').trim(),
20842102
name: String(runtime?.info?.Name || source.name || key).trim() || key,
20852103
icon: source.icon || runtime?.Labels?.['net.unraid.docker.icon'] || '/plugins/dynamix.docker.manager/images/question.png',
2086-
webui: String(source.webui || runtimeState.WebUi || runtimeState.TSWebUi || '').trim(),
2104+
webui: resolvePreferredWebuiValue(runtimeState.WebUi, runtimeState.TSWebUi, source.webui),
20872105
shell: source.shell || runtime?.info?.Shell || '/bin/sh',
20882106
pause: hasRuntimePause ? (runtimeState.Paused === true) : (source.pause === true),
20892107
state: hasRuntimeState ? (runtimeState.Running === true) : (source.state === true),
@@ -2101,13 +2119,10 @@ const getFolderRuntimeContainers = (folder) => {
21012119
return {};
21022120
}
21032121
const runtime = folder.runtimeContainers;
2104-
if (runtime && typeof runtime === 'object' && !Array.isArray(runtime) && Object.keys(runtime).length > 0) {
2105-
return runtime;
2106-
}
21072122
const containers = folder.containers;
21082123
const names = readFolderContainerNames(containers);
21092124
if (!names.length) {
2110-
return {};
2125+
return (runtime && typeof runtime === 'object' && !Array.isArray(runtime)) ? runtime : {};
21112126
}
21122127
const sourceMap = (containers && typeof containers === 'object' && !Array.isArray(containers)) ? containers : {};
21132128
const collected = {};
@@ -2117,6 +2132,7 @@ const getFolderRuntimeContainers = (folder) => {
21172132
}
21182133
collected[name] = buildRuntimeContainerEntry(name, sourceMap[name]);
21192134
}
2135+
folder.runtimeContainers = collected;
21202136
return collected;
21212137
};
21222138

@@ -2607,8 +2623,8 @@ const syncDockerVisibleFoldersFromRuntimeCache = () => {
26072623
const runtimeContainers = folderHasChildren(id)
26082624
? buildRuntimeContainerMapForFolder(id, true)
26092625
: getFolderRuntimeContainers(folder);
2626+
folder.runtimeContainers = runtimeContainers;
26102627
if (folderHasChildren(id)) {
2611-
folder.runtimeContainers = runtimeContainers;
26122628
syncParentFolderVisualState(id, folder?.status?.expanded === true);
26132629
}
26142630
updateFolderRowStatusFromContainers(id, folder, runtimeContainers);
@@ -2618,6 +2634,18 @@ const syncDockerVisibleFoldersFromRuntimeCache = () => {
26182634
applyDockerFocusedFolderState();
26192635
};
26202636

2637+
const buildDockerWebuiSignature = (source) => {
2638+
const map = source && typeof source === 'object' ? source : {};
2639+
const names = Object.keys(map).sort((a, b) => a.localeCompare(b));
2640+
if (!names.length) {
2641+
return '';
2642+
}
2643+
return names.map((name) => {
2644+
const state = map[name]?.info?.State && typeof map[name].info.State === 'object' ? map[name].info.State : {};
2645+
return `${name}:${getSafeWebuiUrl(state.WebUi)}|${getSafeWebuiUrl(state.TSWebUi)}`;
2646+
}).join('|');
2647+
};
2648+
26212649
const queueDockerDeferredRuntimeInfoHydration = (generation, stateSignature, fullInfoPromise = null) => {
26222650
const requestPromise = fullInfoPromise && typeof fullInfoPromise.then === 'function'
26232651
? fullInfoPromise
@@ -2640,12 +2668,18 @@ const queueDockerDeferredRuntimeInfoHydration = (generation, stateSignature, ful
26402668
if (!parsed || Object.keys(parsed).length <= 0) {
26412669
return;
26422670
}
2671+
const previousWebuiSignature = buildDockerWebuiSignature(dockerRuntimeInfoByName);
26432672
dockerRuntimeInfoByName = normalizeDockerRuntimeInfoMap(parsed, dockerRuntimeInfoByName);
2673+
const nextWebuiSignature = buildDockerWebuiSignature(dockerRuntimeInfoByName);
26442674
if (stateSignature) {
26452675
lastLiveRefreshStateSignature = stateSignature;
26462676
}
26472677
markDockerFatalBannerStep('Docker runtime details hydrated');
26482678
recordDockerFatalBannerAction('Docker runtime details hydrated');
2679+
if (previousWebuiSignature !== nextWebuiSignature) {
2680+
queueLoadlistRefresh();
2681+
return;
2682+
}
26492683
syncDockerVisibleFoldersFromRuntimeCache();
26502684
})
26512685
.catch(() => {});

tests/performance-optimizations.test.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,8 @@ test('runtime refresh uses lightweight state mode checks before re-rendering', (
125125
assert.match(dashboardJs, /const queueCreateFoldersRender = \(\) =>/);
126126
assert.match(dockerJs, /const readDockerHostOrderFromDom = \(\) =>/);
127127
assert.match(dockerJs, /const queueDockerDeferredRuntimeInfoHydration = \(generation,\s*stateSignature,\s*fullInfoPromise = null\) =>/);
128+
assert.match(dockerJs, /const buildDockerWebuiSignature = \(source\) =>/);
129+
assert.match(dockerJs, /if \(previousWebuiSignature !== nextWebuiSignature\) \{/);
128130
assert.match(dockerJs, /const yieldDockerRenderLoop = async \(processedCount,\s*totalCount\) =>/);
129131
assert.match(dockerJs, /render:\s*\[[\s\S]*read_info\.php\?type=docker&mode=state/);
130132
assert.match(dockerJs, /fullInfo:\s*createDockerRuntimeRequest\('\/plugins\/folderview\.plus\/server\/read_info\.php\?type=docker'/);

tests/ui-smoke-layout.test.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,8 @@ test('nested folder expansion avoids duplicate parent previews and keeps child-o
354354
assert.match(dockerJs, /const allowLogsQuickAction = nestedParentPreview \|\| quickActionPrefs\.preview_logs === true;/);
355355
assert.match(dockerJs, /const shouldRenderPreviewWebuiPlaceholder = \(settings = \{\}, webuiQuickActionEnabled = false\) =>/);
356356
assert.match(dockerJs, /const appendPreviewWebuiPlaceholder = \(\$target\) =>/);
357+
assert.match(dockerJs, /const hasUnresolvedWebuiTemplateTokens = \(value\) =>/);
358+
assert.match(dockerJs, /const resolvePreferredWebuiValue = \(\.\.\.candidates\) =>/);
357359
assert.match(dockerJs, /shouldRenderPreviewWebuiPlaceholder\(folder\.settings,\s*folder\.settings\.preview_webui === true\)/);
358360
assert.match(dockerJs, /shouldRenderPreviewWebuiPlaceholder\(folder\?\.settings \|\| \{\},\s*allowWebuiQuickAction\)/);
359361
assert.match(dockerJs, /const previewWebuiUrl = getSafeWebuiUrl\(newFolder\[container_name_in_folder\]\?\.webui \|\| ct\.info\.State\.WebUi \|\| ct\.info\.State\.TSWebUi \|\| ''\);/);

0 commit comments

Comments
 (0)