Skip to content

Commit 9ee7bda

Browse files
author
FolderView Plus Test
committed
Fix docker privacy toggle and page view save races
1 parent 3184ffc commit 9ee7bda

9 files changed

Lines changed: 138 additions & 35 deletions

File tree

archive/folderview.plus-2026.04.15.12.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+
6869a7ccc83155cb777821a907a134dd4d5dc482c00478184751bddf3f097b0a folderview.plus-2026.04.16.04.txz

folderview.plus.plg

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,20 @@
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.04.16.03">
10-
<!ENTITY md5 "04930b9b7e5ebee2bf3c4f6057009781">
9+
<!ENTITY version "2026.04.16.04">
10+
<!ENTITY md5 "5226914f789a960af5b2975c5dd4bba4">
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.04.16.04
17+
- Fix: Docker support-bundle snapshots, trace storage caps, and rendered-state diagnostics.
18+
- Fix: Docker runtime rows, folder state, and container interactions.
19+
- UX: Settings workspace layout, section flows, and table behavior.
20+
- Quality: Release automation, CI smoke coverage, and packaging guards.
21+
22+
1623
###2026.04.16.03
1724
- Fix: VM runtime rows, folder state, and VM actions.
1825
- UX: Dashboard layouts, quick rails, and folder card interactions.

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

Lines changed: 57 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3201,6 +3201,9 @@ const DOCKER_RUNTIME_PRIVACY_TOGGLE_SHELL_ID = 'fvplus-docker-runtime-privacy-sh
32013201
const DOCKER_RUNTIME_PRIVACY_TOGGLE_FALLBACK_HOST_ID = 'fvplus-docker-runtime-toolbar-controls';
32023202
const DOCKER_LEGACY_HOST_BOOTSTRAP_RENDER_COMPAT = false;
32033203
let dockerRuntimePrivacyToggleMountQueued = false;
3204+
let dockerRuntimePrivacyPersistPromise = null;
3205+
let dockerRuntimePrivacyPendingEnabled = null;
3206+
let dockerRuntimePrivacyPersistedPrefs = null;
32043207

32053208
const readDockerRuntimePrivacyMode = () => utils.normalizePrefs(folderTypePrefs || {}).dashboard?.privacyMode === true;
32063209

@@ -3249,18 +3252,22 @@ const findDockerRuntimeListViewToggleAnchor = () => {
32493252
].filter(Boolean);
32503253
const switchSelector = 'input[type="checkbox"], .switch-button, .switch-button-background';
32513254
for (const scope of scopes) {
3252-
const candidates = Array.from(scope.querySelectorAll('label, div, span, td, th'));
3253-
for (const candidate of candidates) {
3254-
const text = String(candidate.textContent || '').replace(/\s+/g, ' ').trim().toLowerCase();
3255-
if (!text.includes('basic view')) {
3256-
continue;
3257-
}
3258-
if (candidate.querySelector(switchSelector)) {
3255+
const switches = Array.from(scope.querySelectorAll(switchSelector));
3256+
for (const toggleNode of switches) {
3257+
const candidates = [
3258+
toggleNode.closest('label'),
3259+
toggleNode.closest('span'),
3260+
toggleNode.closest('div'),
3261+
toggleNode.parentElement,
3262+
toggleNode.parentElement?.parentElement
3263+
].filter(Boolean);
3264+
for (const candidate of candidates) {
3265+
const text = String(candidate.textContent || '').replace(/\s+/g, ' ').trim().toLowerCase();
3266+
if (!text.includes('basic view')) {
3267+
continue;
3268+
}
32593269
return candidate;
32603270
}
3261-
if (candidate.parentElement && candidate.parentElement.querySelector(switchSelector)) {
3262-
return candidate.parentElement;
3263-
}
32643271
}
32653272
}
32663273
return null;
@@ -3320,7 +3327,7 @@ const renderDockerRuntimePrivacyToggle = () => {
33203327
shell = document.createElement('div');
33213328
shell.id = DOCKER_RUNTIME_PRIVACY_TOGGLE_SHELL_ID;
33223329
}
3323-
shell.className = `fvplus-docker-runtime-toggle-shell${mount.fallback ? ' is-fallback' : ''}`;
3330+
shell.className = `fvplus-docker-runtime-toggle-shell${mount.anchor ? ' is-inline-cluster' : ''}${mount.fallback ? ' is-fallback' : ''}`;
33243331
if (mount.anchor) {
33253332
if (mount.anchor.nextElementSibling !== shell) {
33263333
mount.anchor.insertAdjacentElement('afterend', shell);
@@ -3329,9 +3336,10 @@ const renderDockerRuntimePrivacyToggle = () => {
33293336
mount.host.insertBefore(shell, mount.host.firstChild);
33303337
}
33313338
const enabled = readDockerRuntimePrivacyMode();
3339+
const savePending = dockerRuntimePrivacyPersistPromise !== null;
33323340
shell.innerHTML = `
33333341
<span class="fvplus-docker-runtime-toggle-label">Privacy</span>
3334-
<input id="${DOCKER_RUNTIME_PRIVACY_TOGGLE_ID}" class="basic-switch fvplus-docker-runtime-privacy-switch" type="checkbox" ${enabled ? 'checked' : ''}>
3342+
<input id="${DOCKER_RUNTIME_PRIVACY_TOGGLE_ID}" class="basic-switch fvplus-docker-runtime-privacy-switch" type="checkbox" ${enabled ? 'checked' : ''} ${savePending ? 'disabled' : ''}>
33353343
`;
33363344
const $input = $(`#${DOCKER_RUNTIME_PRIVACY_TOGGLE_ID}`);
33373345
if (!$input.length) {
@@ -3366,36 +3374,57 @@ const queueDockerRuntimePrivacyToggleMount = () => {
33663374
setTimeout(flush, 0);
33673375
};
33683376

3377+
const getDockerRuntimePersistedPrefs = () => utils.normalizePrefs(dockerRuntimePrivacyPersistedPrefs || folderTypePrefs || {});
3378+
3379+
const flushDockerRuntimePrivacyModePersistence = async () => {
3380+
if (dockerRuntimePrivacyPersistPromise) {
3381+
return dockerRuntimePrivacyPersistPromise;
3382+
}
3383+
dockerRuntimePrivacyPersistPromise = (async () => {
3384+
try {
3385+
while (dockerRuntimePrivacyPendingEnabled !== null) {
3386+
const targetEnabled = dockerRuntimePrivacyPendingEnabled === true;
3387+
dockerRuntimePrivacyPendingEnabled = null;
3388+
const response = await persistDockerRuntimePrivacyMode(targetEnabled);
3389+
folderTypePrefs = utils.normalizePrefs(response?.prefs || buildDockerRuntimePrivacyPrefsPayload(targetEnabled));
3390+
dockerRuntimePrivacyPersistedPrefs = folderTypePrefs;
3391+
applyRuntimePrefs(folderTypePrefs);
3392+
queueDockerRuntimePrivacyToggleMount();
3393+
}
3394+
} finally {
3395+
dockerRuntimePrivacyPersistPromise = null;
3396+
queueDockerRuntimePrivacyToggleMount();
3397+
}
3398+
})();
3399+
return dockerRuntimePrivacyPersistPromise;
3400+
};
3401+
33693402
const setDockerRuntimePrivacyMode = async (enabled, options = {}) => {
33703403
const nextEnabled = enabled === true;
3371-
const previousPrefs = utils.normalizePrefs(folderTypePrefs || {});
3404+
const previousPrefs = getDockerRuntimePersistedPrefs();
33723405
const previousEnabled = previousPrefs.dashboard?.privacyMode === true;
3373-
if (previousEnabled === nextEnabled) {
3406+
if (readDockerRuntimePrivacyMode() === nextEnabled && dockerRuntimePrivacyPendingEnabled === null && !dockerRuntimePrivacyPersistPromise) {
33743407
queueDockerRuntimePrivacyToggleMount();
33753408
return;
33763409
}
3410+
dockerRuntimePrivacyPendingEnabled = nextEnabled;
33773411
folderTypePrefs = buildDockerRuntimePrivacyPrefsPayload(nextEnabled);
33783412
applyRuntimePrefs(folderTypePrefs);
33793413
queueDockerRuntimePrivacyToggleMount();
33803414
if (options.persist === false) {
33813415
return;
33823416
}
3383-
const result = await dockerSafeUiActionRunner.run('docker-runtime-privacy-toggle', async () => {
3384-
const response = await persistDockerRuntimePrivacyMode(nextEnabled);
3385-
folderTypePrefs = utils.normalizePrefs(response?.prefs || folderTypePrefs);
3386-
applyRuntimePrefs(folderTypePrefs);
3387-
queueDockerRuntimePrivacyToggleMount();
3388-
return response;
3389-
}, {
3390-
userVisible: false
3391-
});
3392-
if (!result.ok) {
3417+
try {
3418+
await flushDockerRuntimePrivacyModePersistence();
3419+
} catch (error) {
3420+
dockerRuntimePrivacyPendingEnabled = null;
33933421
folderTypePrefs = previousPrefs;
3422+
dockerRuntimePrivacyPersistedPrefs = previousPrefs;
33943423
applyRuntimePrefs(folderTypePrefs);
33953424
queueDockerRuntimePrivacyToggleMount();
33963425
swal({
33973426
title: 'Privacy toggle save failed',
3398-
text: `${escapeHtml(String(result.error?.message || 'FolderView Plus could not save the Docker privacy toggle.'))}<br>Reverted to the previous state.`,
3427+
text: `${escapeHtml(String(error?.message || 'FolderView Plus could not save the Docker privacy toggle.'))}<br>Reverted to the previous state.`,
33993428
type: 'error',
34003429
html: true,
34013430
confirmButtonText: 'OK'
@@ -6239,6 +6268,9 @@ const scheduleLiveRefresh = (prefs) => {
62396268
const applyRuntimePrefs = (prefs) => {
62406269
const normalized = utils.normalizePrefs(prefs || {});
62416270
lastAppliedRuntimePrefs = normalized;
6271+
if (!dockerRuntimePrivacyPersistPromise && dockerRuntimePrivacyPendingEnabled === null) {
6272+
dockerRuntimePrivacyPersistedPrefs = normalized;
6273+
}
62426274
if (document.body && typeof document.body.setAttribute === 'function') {
62436275
document.body.setAttribute('data-fvplus-docker-page-view', resolveDockerPageViewMode(normalized));
62446276
}
@@ -6402,4 +6434,3 @@ if (FOLDER_VIEW_DEBUG_MODE) console.log('[FV3_DEBUG] docker.js: End of script ex
64026434

64036435

64046436

6405-

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

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,16 @@ let prefsByType = {
442442
docker: utils.normalizePrefs({}),
443443
vm: utils.normalizePrefs({})
444444
};
445+
const runtimePrefsSaveStateByType = {
446+
docker: {
447+
revision: 0,
448+
lastCommittedPrefs: utils.normalizePrefs({})
449+
},
450+
vm: {
451+
revision: 0,
452+
lastCommittedPrefs: utils.normalizePrefs({})
453+
}
454+
};
445455
let infoByType = {
446456
docker: {},
447457
vm: {}
@@ -7932,8 +7942,23 @@ const toggleFolderPin = async (type, folderId) => {
79327942
}
79337943
};
79347944

7945+
const getRuntimePrefsSaveState = (type) => {
7946+
const resolvedType = type === 'vm' ? 'vm' : 'docker';
7947+
if (!runtimePrefsSaveStateByType[resolvedType] || typeof runtimePrefsSaveStateByType[resolvedType] !== 'object') {
7948+
runtimePrefsSaveStateByType[resolvedType] = {
7949+
revision: 0,
7950+
lastCommittedPrefs: utils.normalizePrefs(prefsByType[resolvedType] || {})
7951+
};
7952+
}
7953+
if (!runtimePrefsSaveStateByType[resolvedType].lastCommittedPrefs) {
7954+
runtimePrefsSaveStateByType[resolvedType].lastCommittedPrefs = utils.normalizePrefs(prefsByType[resolvedType] || {});
7955+
}
7956+
return runtimePrefsSaveStateByType[resolvedType];
7957+
};
7958+
79357959
const changeRuntimePref = async (type, key, value) => {
79367960
const current = utils.normalizePrefs(prefsByType[type]);
7961+
const runtimeSaveState = getRuntimePrefsSaveState(type);
79377962
const next = {
79387963
...current
79397964
};
@@ -7962,15 +7987,28 @@ const changeRuntimePref = async (type, key, value) => {
79627987
if (key === 'liveRefreshEnabled' || key === 'lazyPreviewEnabled') {
79637988
syncRuntimeDependentFields(type);
79647989
}
7990+
prefsByType[type] = utils.normalizePrefs(next);
7991+
renderRuntimeControls(type);
7992+
const requestRevision = runtimeSaveState.revision + 1;
7993+
runtimeSaveState.revision = requestRevision;
79657994

79667995
try {
7967-
prefsByType[type] = await postPrefs(type, next);
7996+
const savedPrefs = await postPrefs(type, next);
7997+
runtimeSaveState.lastCommittedPrefs = utils.normalizePrefs(savedPrefs);
7998+
if (requestRevision !== runtimeSaveState.revision) {
7999+
return;
8000+
}
8001+
prefsByType[type] = runtimeSaveState.lastCommittedPrefs;
79688002
renderRuntimeControls(type);
79698003
if (key === 'themeCompatibilityMode') {
79708004
applySettingsResolvedThemeTokens(`pref-${type}`);
79718005
queueSettingsThemeAwareReflow(`theme-compat-${type}`);
79728006
}
79738007
} catch (error) {
8008+
if (requestRevision !== runtimeSaveState.revision) {
8009+
return;
8010+
}
8011+
prefsByType[type] = utils.normalizePrefs(runtimeSaveState.lastCommittedPrefs || current);
79748012
renderRuntimeControls(type);
79758013
showError('Runtime preference save failed', error);
79768014
}
@@ -9318,5 +9356,3 @@ settingsActionSupportModule.registerWindowActions(window, {
93189356

93199357

93209358

9321-
9322-

src/folderview.plus/usr/local/emhttp/plugins/folderview.plus/styles/docker.css

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,13 @@ body.fvplus-privacy-docker-runtime #docker_view td.ct-name img.img,
288288
body.fvplus-privacy-docker-runtime #docker_list .folder-preview .folder-img,
289289
body.fvplus-privacy-docker-runtime #docker_view .folder-preview .folder-img,
290290
body.fvplus-privacy-docker-runtime .preview-img > .folder-img,
291-
body.fvplus-privacy-docker-runtime .fv-docker-member-menu-icon {
291+
body.fvplus-privacy-docker-runtime .fv-docker-member-menu-icon,
292+
body.fvplus-privacy-docker-runtime .fv-docker-command-card-title-icon,
293+
body.fvplus-privacy-docker-runtime .fv-docker-command-member-icon,
294+
body.fvplus-privacy-docker-runtime .fv-docker-tree-explorer-node-icon,
295+
body.fvplus-privacy-docker-runtime .fv-docker-tree-explorer-card-title-icon,
296+
body.fvplus-privacy-docker-runtime .fv-docker-tree-explorer-child-icon,
297+
body.fvplus-privacy-docker-runtime .fv-docker-tree-explorer-member-icon {
292298
filter: blur(0.4rem);
293299
}
294300

@@ -299,7 +305,13 @@ body.fvplus-privacy-docker-runtime #docker_view td.ct-name .folder-appname,
299305
body.fvplus-privacy-docker-runtime #docker_list .folder-preview .appname,
300306
body.fvplus-privacy-docker-runtime #docker_view .folder-preview .appname,
301307
body.fvplus-privacy-docker-runtime .preview-actual-name,
302-
body.fvplus-privacy-docker-runtime .fv-docker-member-menu-name {
308+
body.fvplus-privacy-docker-runtime .fv-docker-member-menu-name,
309+
body.fvplus-privacy-docker-runtime .fv-docker-command-card-title,
310+
body.fvplus-privacy-docker-runtime .fv-docker-command-member-pill,
311+
body.fvplus-privacy-docker-runtime .fv-docker-tree-explorer-node-copy,
312+
body.fvplus-privacy-docker-runtime .fv-docker-tree-explorer-detail-title,
313+
body.fvplus-privacy-docker-runtime .fv-docker-tree-explorer-child-copy,
314+
body.fvplus-privacy-docker-runtime .fv-docker-tree-explorer-member-pill {
303315
filter: blur(0.28rem);
304316
}
305317

tests/privacy-mode-contract.test.mjs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@ test('privacy mode toggles runtime body classes and ships masking selectors acro
4343
assert.match(dockerJs, /const DOCKER_RUNTIME_PRIVACY_TOGGLE_ID = 'fvplus-docker-runtime-privacy-toggle';/);
4444
assert.match(dockerJs, /const renderDockerRuntimePrivacyToggle = \(\) =>/);
4545
assert.match(dockerJs, /const findDockerRuntimeListViewToggleAnchor = \(\) =>/);
46+
assert.match(dockerJs, /let dockerRuntimePrivacyPersistPromise = null;/);
47+
assert.match(dockerJs, /let dockerRuntimePrivacyPendingEnabled = null;/);
48+
assert.match(dockerJs, /const flushDockerRuntimePrivacyModePersistence = async \(\) =>/);
49+
assert.match(dockerJs, /shell\.className = `fvplus-docker-runtime-toggle-shell\$\{mount\.anchor \? ' is-inline-cluster' : ''\}\$\{mount\.fallback \? ' is-fallback' : ''\}`;/);
50+
assert.match(dockerJs, /if \(readDockerRuntimePrivacyMode\(\) === nextEnabled && dockerRuntimePrivacyPendingEnabled === null && !dockerRuntimePrivacyPersistPromise\) \{/);
4651
assert.match(dockerJs, /const setDockerRuntimePrivacyMode = async \(enabled, options = \{\}\) =>/);
4752
assert.match(dockerJs, /queueDockerRuntimePrivacyToggleMount\(\);/);
4853
assert.match(vmJs, /toggleClass\('fvplus-privacy-vm-runtime', normalized\?\.dashboard\?\.privacyMode === true\)/);
@@ -62,6 +67,12 @@ test('privacy mode toggles runtime body classes and ships masking selectors acro
6267
assert.match(dockerCss, /\.fvplus-docker-runtime-toggle-shell\.is-inline-cluster/);
6368
assert.match(dockerCss, /\.fvplus-docker-runtime-toggle-label/);
6469
assert.match(dockerCss, /\.fvplus-docker-runtime-toolbar-controls/);
70+
assert.match(dockerCss, /\.fv-docker-command-card-title-icon/);
71+
assert.match(dockerCss, /\.fv-docker-command-member-icon/);
72+
assert.match(dockerCss, /\.fv-docker-command-member-pill/);
73+
assert.match(dockerCss, /\.fv-docker-tree-explorer-node-icon/);
74+
assert.match(dockerCss, /\.fv-docker-tree-explorer-detail-title/);
75+
assert.match(dockerCss, /\.fv-docker-tree-explorer-member-pill/);
6576
assert.match(dockerCss, /\.fv-docker-member-menu-name/);
6677
assert.match(vmCss, /body\.fvplus-privacy-vm-runtime/);
6778
assert.match(dashboardCss, /body\.fvplus-privacy-docker-dashboard/);

tests/settings-bindings.test.mjs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,8 +146,14 @@ test('settings page exposes theme fallback controls and runtime self-heal action
146146
assert.match(script, /const runThemeSelfHeal = async \(\) =>/);
147147
assert.match(script, /run_theme_self_heal/);
148148
assert.match(script, /registerWindowActions\(window,\s*\{[\s\S]*runThemeSelfHeal[\s\S]*\}\);/);
149+
assert.match(script, /const runtimePrefsSaveStateByType = \{/);
150+
assert.match(script, /const getRuntimePrefsSaveState = \(type\) => \{/);
151+
assert.match(script, /const requestRevision = runtimeSaveState\.revision \+ 1;/);
152+
assert.match(script, /if \(requestRevision !== runtimeSaveState\.revision\) \{\s*return;\s*\}/);
153+
assert.match(script, /runtimeSaveState\.lastCommittedPrefs = utils\.normalizePrefs\(savedPrefs\);/);
149154
assert.match(script, /else if \(key === 'pageViewMode'\) \{/);
150-
assert.match(script, /catch \(error\) \{\s*renderVisibilityControls\(type\);[\s\S]*showError\('Visibility preference save failed', error\);/);
155+
assert.match(script, /prefsByType\[type\] = utils\.normalizePrefs\(next\);\s*renderRuntimeControls\(type\);/);
156+
assert.match(script, /catch \(error\) \{\s*if \(requestRevision !== runtimeSaveState\.revision\) \{\s*return;\s*\}[\s\S]*showError\('Runtime preference save failed', error\);/);
151157
assert.match(script, /else if \(key === 'themeCompatibilityMode'\) \{/);
152158
assert.match(libPrefsPhp, /function normalizeRuntimePageViewMode\(\$value\): string \{[\s\S]*\['folderview', 'host', 'command', 'tree-explorer'\]/);
153159
});

0 commit comments

Comments
 (0)