Skip to content

Commit dd2bb55

Browse files
Fix docker bootstrap double reload
1 parent 55f8fde commit dd2bb55

11 files changed

Lines changed: 42 additions & 32 deletions

archive/folderview.plus-2026.03.29.18.txz.sha256

Lines changed: 0 additions & 1 deletion
This file was deleted.
-14.2 MB
Binary file not shown.

archive/folderview.plus-2026.03.29.19.txz.sha256

Lines changed: 0 additions & 1 deletion
This file was deleted.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
6c7db1b00df931a36548119b76b133e0937729bd89902172674603c866031138 folderview.plus-2026.03.30.19.txz

folderview.plus.plg

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,19 @@
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.30.17">
10-
<!ENTITY md5 "b08a5b0aa4659eef6a3e4eafd57de99c">
9+
<!ENTITY version "2026.03.30.19">
10+
<!ENTITY md5 "333b926b727ed81bb85f4140c142f060">
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.30.19
17+
- Fix: Stop Docker bootstrap from forcing a second host reload when Unraid stages a newer request bundle while FolderView Plus is still rendering folders.
18+
- Fix: Drain queued Docker folder renders in place against the latest staged request bundle instead of scheduling another `loadlist()` cycle after the page has already refreshed.
19+
- UX: Suppress the extra Docker runtime loading shell when a queued bootstrap render replays in place so the page no longer flashes a second reload.
20+
- Regression guard: Cover the primed-request `listview()`/`loadlist()` bootstrap path and the deferred runtime hydration path so Docker refreshes stay in-place.
21+
1622
###2026.03.30.17
1723
- Fix: Stop Docker advanced preview context popups configured for click from eager-opening on the first hover interaction.
1824
- Quality: Trim the lazy tooltip trigger-mode fix so the Docker runtime stays inside the repo perf budget while preserving hover-mode eager-open behavior.

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

Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2812,18 +2812,6 @@ const syncDockerVisibleFoldersFromRuntimeCache = () => {
28122812
applyDockerFocusedFolderState();
28132813
};
28142814

2815-
const buildDockerWebuiSignature = (source) => {
2816-
const map = source && typeof source === 'object' ? source : {};
2817-
const names = Object.keys(map).sort((a, b) => a.localeCompare(b));
2818-
if (!names.length) {
2819-
return '';
2820-
}
2821-
return names.map((name) => {
2822-
const state = map[name]?.info?.State && typeof map[name].info.State === 'object' ? map[name].info.State : {};
2823-
return `${name}:${getSafeWebuiUrl(state.WebUi)}|${getSafeWebuiUrl(state.TSWebUi)}|${state.Updated === false ? 'u' : (state.Updated === true ? 'c' : '?')}`;
2824-
}).join('|');
2825-
};
2826-
28272815
const queueDockerDeferredRuntimeInfoHydration = (generation, stateSignature, fullInfoPromise = null) => {
28282816
const requestPromise = fullInfoPromise && typeof fullInfoPromise.then === 'function'
28292817
? fullInfoPromise
@@ -2846,18 +2834,12 @@ const queueDockerDeferredRuntimeInfoHydration = (generation, stateSignature, ful
28462834
if (!parsed || Object.keys(parsed).length <= 0) {
28472835
return;
28482836
}
2849-
const previousWebuiSignature = buildDockerWebuiSignature(dockerRuntimeInfoByName);
28502837
dockerRuntimeInfoByName = normalizeDockerRuntimeInfoMap(parsed, dockerRuntimeInfoByName);
2851-
const nextWebuiSignature = buildDockerWebuiSignature(dockerRuntimeInfoByName);
28522838
if (stateSignature) {
28532839
lastLiveRefreshStateSignature = stateSignature;
28542840
}
28552841
markDockerFatalBannerStep('Docker runtime details hydrated');
28562842
recordDockerFatalBannerAction('Docker runtime details hydrated');
2857-
if (previousWebuiSignature !== nextWebuiSignature) {
2858-
queueLoadlistRefresh();
2859-
return;
2860-
}
28612843
syncDockerVisibleFoldersFromRuntimeCache();
28622844
})
28632845
.catch(() => {});
@@ -3185,7 +3167,10 @@ const queueCreateFoldersRender = () => {
31853167
createFoldersInFlight = false;
31863168
if (createFoldersQueued) {
31873169
createFoldersQueued = false;
3188-
queueLoadlistRefresh();
3170+
// If Unraid queued a newer request bundle mid-render, replay FolderView Plus
3171+
// against the current DOM instead of forcing another host loadlist() cycle.
3172+
nextDockerRenderSuppressLoadingUi = true;
3173+
queueCreateFoldersRender();
31893174
}
31903175
});
31913176
};
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import test from 'node:test';
2+
import assert from 'node:assert/strict';
3+
import fs from 'node:fs';
4+
import path from 'node:path';
5+
6+
const repoRoot = path.resolve(process.cwd());
7+
const dockerJs = fs.readFileSync(
8+
path.join(repoRoot, 'src/folderview.plus/usr/local/emhttp/plugins/folderview.plus/scripts/docker.js'),
9+
'utf8'
10+
);
11+
12+
test('docker queued bootstrap renders replay in place instead of forcing a second host loadlist', () => {
13+
assert.match(dockerJs, /\/\/ Prime requests for environments where loadlist isn't called first\.\s*folderReq = buildDockerFolderReq\(\);/);
14+
assert.match(dockerJs, /window\.loadlist = \(\) => \{[\s\S]*?loadedFolder = false;[\s\S]*?folderReq = buildDockerFolderReq\(\);/);
15+
assert.match(dockerJs, /window\.listview = \(\) => \{[\s\S]*?if \(!loadedFolder\) \{[\s\S]*?queueCreateFoldersRender\(\);[\s\S]*?loadedFolder = true;/);
16+
assert.match(dockerJs, /const queueCreateFoldersRender = \(\) => \{[\s\S]*?if \(createFoldersQueued\) \{\s*createFoldersQueued = false;[\s\S]*?nextDockerRenderSuppressLoadingUi = true;\s*queueCreateFoldersRender\(\);\s*\}[\s\S]*?\};/);
17+
assert.doesNotMatch(dockerJs, /const queueCreateFoldersRender = \(\) => \{[\s\S]*?if \(createFoldersQueued\) \{\s*createFoldersQueued = false;\s*queueLoadlistRefresh\(\);\s*\}[\s\S]*?\};/);
18+
});

tests/docker-folder-row-quick-actions.test.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ test('docker hydration refreshes existing preview actions in place instead of re
3939
assert.match(dockerScript, /const syncDockerLeafFolderPreviewActions = \(id,\s*folder,\s*runtimeContainers\) =>/);
4040
assert.match(dockerScript, /syncDockerLeafFolderPreviewActions\(id,\s*folder,\s*runtimeContainers\);/);
4141
assert.match(dockerScript, /\$preview\.find\('\[id\^="folder-preview-"\]'\)\.each\(\(_,\s*node\) => \{\s*\$\(node\)\.data\('fvTooltipLazyBuilt', false\);/s);
42+
assert.match(dockerScript, /const queueDockerDeferredRuntimeInfoHydration = \(generation,\s*stateSignature,\s*fullInfoPromise = null\) => \{[\s\S]*?syncDockerVisibleFoldersFromRuntimeCache\(\);[\s\S]*?\}\)\s*\.catch\(\(\) => \{\}\);/);
43+
assert.doesNotMatch(dockerScript, /const queueDockerDeferredRuntimeInfoHydration = \(generation,\s*stateSignature,\s*fullInfoPromise = null\) => \{[\s\S]*?const previousWebuiSignature/);
4244
});
4345

4446
test('docker hydration refresh updates collapsed folder update columns from runtime cache', () => {

tests/docker-update-status-regression.test.mjs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,10 @@ test('docker runtime preserves hydrated update flags when normalizing partial ru
1313
assert.match(dockerJs, /Updated:\s*sourceState\.Updated \?\? previousState\.Updated \?\? null/);
1414
});
1515

16-
test('deferred docker runtime hydration rerenders when update availability changes', () => {
17-
assert.match(dockerJs, /\|\$\{state\.Updated === false \? 'u' : \(state\.Updated === true \? 'c' : '\?'\)\}/);
18-
assert.match(dockerJs, /const previousWebuiSignature = buildDockerWebuiSignature\(dockerRuntimeInfoByName\);/);
19-
assert.match(dockerJs, /const nextWebuiSignature = buildDockerWebuiSignature\(dockerRuntimeInfoByName\);/);
20-
assert.match(dockerJs, /previousWebuiSignature !== nextWebuiSignature/);
21-
assert.match(dockerJs, /queueLoadlistRefresh\(\);\s*return;/);
16+
test('deferred docker runtime hydration refreshes visible folder state in place instead of reloading the page', () => {
17+
assert.match(dockerJs, /const queueDockerDeferredRuntimeInfoHydration = \(generation,\s*stateSignature,\s*fullInfoPromise = null\) => \{[\s\S]*?dockerRuntimeInfoByName = normalizeDockerRuntimeInfoMap\(parsed,\s*dockerRuntimeInfoByName\);[\s\S]*?markDockerFatalBannerStep\('Docker runtime details hydrated'\);[\s\S]*?recordDockerFatalBannerAction\('Docker runtime details hydrated'\);[\s\S]*?syncDockerVisibleFoldersFromRuntimeCache\(\);[\s\S]*?\}\)\s*\.catch\(\(\) => \{\}\);/);
18+
assert.doesNotMatch(dockerJs, /const queueDockerDeferredRuntimeInfoHydration = \(generation,\s*stateSignature,\s*fullInfoPromise = null\) => \{[\s\S]*?const previousWebuiSignature/);
19+
assert.doesNotMatch(dockerJs, /const queueDockerDeferredRuntimeInfoHydration = \(generation,\s*stateSignature,\s*fullInfoPromise = null\) => \{[\s\S]*?const nextWebuiSignature/);
2220
});
2321

2422
test('folder update-column renderer is reused across initial and synced folder state', () => {

0 commit comments

Comments
 (0)