Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file removed archive/folderview.plus-2026.07.24.01.txz
Binary file not shown.
1 change: 0 additions & 1 deletion archive/folderview.plus-2026.07.24.01.txz.sha256

This file was deleted.

Binary file added archive/folderview.plus-2026.07.28.06.txz
Binary file not shown.
1 change: 1 addition & 0 deletions archive/folderview.plus-2026.07.28.06.txz.sha256
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
e78b2e2f7d662058c6b3f603f7ab79783a3b0691f7a624be65180a37e389d3e2 folderview.plus-2026.07.28.06.txz
29 changes: 29 additions & 0 deletions docs/releases/2026.07.28.06.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# FolderView Plus 2026.07.28.06

This reliability update restores folder interactions after the declarative
event security migration and removes false Storage and paths failures caused by
managed or retired installed runtime files.

## Folder interactions

- Fix: Route Docker and VM folder icon, name, and expand controls through one
shared delegated-action controller instead of the declarative compatibility
bridge.
- Fix: Bind each folder action once, remove the legacy action attribute during
row creation, and release the delegated handler when the page is unloaded.
- Diagnostics: Treat the shared folder-action controller as a required runtime
module so a missing or stale asset produces a specific bootstrap finding.

## Runtime integrity

- Diagnostics: Exclude the exact generated `icon-asset-pack.json` activation
record from unexpected executable-file findings while continuing to flag
every other unmanifested JSON, script, style, shell, or PHP file.
- Maintenance: Remove the retired native-organizer and Docker member-menu
scripts from the installed plugin directory during upgrades.

## Regression coverage

- Test: Exercise folder action decoration, single-dispatch behavior, teardown,
Docker and VM module ordering, managed runtime-file handling, obsolete-file
cleanup, browser fixtures, and runtime performance budgets.
2 changes: 1 addition & 1 deletion docs/sbom.cdx.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"component": {
"type": "application",
"name": "FolderView Plus",
"version": "2026.07.28.05",
"version": "2026.07.28.06",
"properties": [
{
"name": "folderview-plus:runtime-dependencies",
Expand Down
8 changes: 4 additions & 4 deletions docs/security/csp-readiness.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"mode": "report-only-shared-unraid-document",
"scope": {
"scannedRoot": "src/folderview.plus/usr/local/emhttp/plugins/folderview.plus",
"scannedFiles": 151,
"scannedFiles": 152,
"exclusions": [
"third-party icon asset pack",
"localization catalogs",
Expand All @@ -20,7 +20,7 @@
"totals": {
"inlineEventAttributes": 0,
"inlineScriptBlocks": 22,
"externalScriptTags": 111,
"externalScriptTags": 113,
"inlineStyleBlocks": 0,
"inlineStyleAttributes": 207,
"evalCalls": 0,
Expand Down Expand Up @@ -96,7 +96,7 @@
"path": "src/folderview.plus/usr/local/emhttp/plugins/folderview.plus/folderview.plus.Docker.page",
"inlineEventAttributes": 0,
"inlineScriptBlocks": 5,
"externalScriptTags": 40,
"externalScriptTags": 41,
"inlineStyleBlocks": 0,
"inlineStyleAttributes": 0,
"evalCalls": 0,
Expand All @@ -108,7 +108,7 @@
"path": "src/folderview.plus/usr/local/emhttp/plugins/folderview.plus/folderview.plus.VMs.page",
"inlineEventAttributes": 0,
"inlineScriptBlocks": 2,
"externalScriptTags": 21,
"externalScriptTags": 22,
"inlineStyleBlocks": 0,
"inlineStyleAttributes": 0,
"evalCalls": 0,
Expand Down
33 changes: 30 additions & 3 deletions folderview.plus.plg
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
<!ENTITY launch "Settings/FolderViewPlus">
<!ENTITY plugdir "/usr/local/emhttp/plugins/&name;">
<!ENTITY pluginURL "https://raw.githubusercontent.com/&github;/dev/folderview.plus.plg">
<!ENTITY version "2026.07.28.05">
<!ENTITY md5 "12f7d8194dbd1db52736c85c9860e650">
<!ENTITY sha256 "91a62bc1615c939ce7deba43247a095594c6bc7b2f09e102c11d1f779616eaf4">
<!ENTITY version "2026.07.28.06">
<!ENTITY md5 "1e64448a5d593649ac77f0e7a234e82c">
<!ENTITY sha256 "e78b2e2f7d662058c6b3f603f7ab79783a3b0691f7a624be65180a37e389d3e2">
<!ENTITY iconPackVersion "1.0.0">
<!ENTITY iconPackMd5 "a149c36b41339949bb5c1eecad550704">
<!ENTITY iconPackSha256 "992f6c3544a8a3c1db80b861472fdd8b3d499f20f81796ed71405a10beb750bd">
Expand All @@ -18,6 +18,31 @@
<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">
<CHANGES>

###2026.07.28.06
# FolderView Plus 2026.07.28.06
This reliability update restores folder interactions after the declarative
event security migration and removes false Storage and paths failures caused by
managed or retired installed runtime files.
## Folder interactions
- Fix: Route Docker and VM folder icon, name, and expand controls through one
shared delegated-action controller instead of the declarative compatibility
bridge.
- Fix: Bind each folder action once, remove the legacy action attribute during
row creation, and release the delegated handler when the page is unloaded.
- Diagnostics: Treat the shared folder-action controller as a required runtime
module so a missing or stale asset produces a specific bootstrap finding.
## Runtime integrity
- Diagnostics: Exclude the exact generated `icon-asset-pack.json` activation
record from unexpected executable-file findings while continuing to flag
every other unmanifested JSON, script, style, shell, or PHP file.
- Maintenance: Remove the retired native-organizer and Docker member-menu
scripts from the installed plugin directory during upgrades.
## Regression coverage
- Test: Exercise folder action decoration, single-dispatch behavior, teardown,
Docker and VM module ordering, managed runtime-file handling, obsolete-file
cleanup, browser fixtures, and runtime performance budgets.


###2026.07.28.05
# FolderView Plus 2026.07.28.05
This security update adds continuous runtime assurance, replay-resistant
Expand Down Expand Up @@ -8032,6 +8057,8 @@ This dev-channel synchronization packages the cross-browser Diagnostics correcti
mkdir -p /boot/config/plugins/&name;/styles
mkdir -p /boot/config/plugins/&name;/scripts
find /boot/config/plugins/&name; -maxdepth 1 -type f -name '&name;-[0-9]*.txz' ! -name '&name;-&version;.txz' -delete
install_stage="obsolete runtime cleanup"
rm -f "&plugdir;/scripts/folderviewplus.native-organizer.js" "&plugdir;/scripts/docker.member-menu.js"
install_stage="icon pack activation"
FVPLUS_ICON_PACK_STATUS_FILE="${icon_status}" /bin/bash "&plugdir;/scripts/install_icon_asset_pack.sh" "&iconPackVersion;" "&iconPackSha256;"
install_stage="scheduled backup registration"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ $fvplusRuntimePreflightHasFatal = runtimePreflightHasFatal($fvplusRuntimePreflig
<script src="<?php fvplus_asset('/plugins/folderview.plus/scripts/runtime.folder-ordering.js')?>"></script>
<script src="<?php fvplus_asset('/plugins/folderview.plus/scripts/docker.runtime.shared.js')?>"></script>
<script src="<?php fvplus_asset('/plugins/folderview.plus/scripts/folder.runtime.state-observers.js')?>"></script>
<script src="<?php fvplus_asset('/plugins/folderview.plus/scripts/folder.runtime.row-actions.js')?>"></script>
<script src="<?php fvplus_asset('/plugins/folderview.plus/scripts/docker.modules.js')?>"></script>
<script src="<?php fvplus_asset('/plugins/folderview.plus/scripts/docker.runtime.info.js')?>"></script>
<script src="<?php fvplus_asset('/plugins/folderview.plus/scripts/docker.runtime.preview-actions.js')?>"></script>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ $fvplusRuntimePreflightHasFatal = runtimePreflightHasFatal($fvplusRuntimePreflig
<script src="<?php fvplus_asset('/plugins/folderview.plus/scripts/runtime.folder-ordering.js')?>"></script>
<script src="<?php fvplus_asset('/plugins/folderview.plus/scripts/docker.runtime.shared.js')?>"></script>
<script src="<?php fvplus_asset('/plugins/folderview.plus/scripts/folder.runtime.state-observers.js')?>"></script>
<script src="<?php fvplus_asset('/plugins/folderview.plus/scripts/folder.runtime.row-actions.js')?>"></script>
<script src="<?php fvplus_asset('/plugins/folderview.plus/scripts/vm.runtime.lifecycle.js')?>"></script>
<script src="<?php fvplus_asset('/plugins/folderview.plus/scripts/vm.js')?>"></script>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,12 @@ if (!runtimeFolderOrdering || typeof runtimeFolderOrdering.createOrderCursor !==
} else {
setDockerFatalBannerModuleStatus('runtime.folder-ordering.js', 'ok', 'folder ordering contract ready');
}
if (typeof dockerRuntimeShared.createFolderRowActionsController !== 'function') {
dockerBootstrapMissingModules.push('folder.runtime.row-actions.js');
setDockerFatalBannerModuleStatus('folder.runtime.row-actions.js', 'missing', 'folder row action lifecycle unavailable');
} else {
setDockerFatalBannerModuleStatus('folder.runtime.row-actions.js', 'ok', 'folder row actions ready');
}
if (
!window.FolderViewDockerRuntimeShared
|| typeof window.FolderViewDockerRuntimeShared.createAsyncActionBoundary !== 'function'
Expand Down Expand Up @@ -5973,6 +5979,7 @@ const createFolder = (folder, id, positionInMainOrder, liveOrderArray, container
const $createdFolderRow = $('#docker_list > tr.folder[data-fv-folder-id]')
.filter((_, element) => String(element.getAttribute('data-fv-folder-id') || '') === id)
.first();
dockerFolderRowActionsController.decorate($createdFolderRow, id);
$createdFolderRow
.attr('data-folder-depth', String(safeDepth))
.find('.folder-name-sub')
Expand Down Expand Up @@ -6982,7 +6989,18 @@ const dropDownButton = (id, persistState = true) => {
hierarchyApi.dropDownButton(id, persistState);
}
};

const dockerFolderRowActionsController = dockerRuntimeShared.createFolderRowActionsController({
document,
$,
namespace: 'fvDockerFolderRowAction',
actionAttribute: 'data-fv-docker-folder-action',
handlers: {
toggle: (id) => dropDownButton(id),
edit: (id) => editFolder(id),
context: (id) => addDockerFolderContext(id)
}
});
const bindDockerFolderRowActions = () => dockerFolderRowActionsController.bind();
/**
* Removie the folder
* @param {string} id the id of the folder
Expand Down Expand Up @@ -8733,7 +8751,7 @@ window.editFolder = editFolder;
window.forceUpdateFolder = forceUpdateFolder;
window.updateFolder = updateFolder;
window.createFolderBtn = createFolderBtn;

bindDockerFolderRowActions();
// This is needed because unraid don't like the folder and the number are set incorrectly, this intercept the request and change the numbers to make the order appear right, this is important for the autostart and to draw the folders
$.ajaxPrefilter((options, originalOptions, jqXHR) => {
if (options.url === "/plugins/dynamix.docker.manager/include/UserPrefs.php") {
Expand Down Expand Up @@ -8810,6 +8828,7 @@ window.addEventListener('pagehide', () => {
clearTimeout(dockerRuntimePrivacyServerReconcileTimer);
clearTimeout(dockerSupportBundlePageSnapshotWriteTimer);
clearTimeout(folderViewPlusDockerStartOrderSyncTimer);
dockerFolderRowActionsController.destroy();
dockerRuntimeResizerObserver?.disconnect?.();
dockerDeferredPreviewController.destroy();
dockerProviderHealthController?.dispose?.();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
(function(root, factory) {
if (typeof module === 'object' && module.exports) {
module.exports = factory();
return;
}
const api = factory();
if (root.FolderViewDockerRuntimeShared && 'object' === typeof root.FolderViewDockerRuntimeShared) {
root.FolderViewDockerRuntimeShared.createFolderRowActionsController = api.createController;
}
}(typeof globalThis !== 'undefined' ? globalThis : this, function() {
const createController = (deps = {}) => {
const jq = deps.$;
const doc = deps.document || (typeof document !== 'undefined' ? document : null);
const namespace = String(deps.namespace || '').replace(/[^A-Za-z0-9]/g, '');
const actionAttribute = String(deps.actionAttribute || '').trim();
const handlers = deps.handlers && typeof deps.handlers === 'object' ? deps.handlers : {};
if (!jq || !doc || !namespace || !/^data-fv-[a-z0-9-]+$/.test(actionAttribute)) {
throw new Error('Folder row action controller dependencies are invalid.');
}

const selector = `[${actionAttribute}]`;
const eventName = `click.${namespace}`;
const decorateTarget = ($row, targetSelector, action, first = false) => {
const $target = first ? $row.find(targetSelector).first() : $row.find(targetSelector);
$target
.removeAttr('data-fv-onclick')
.attr({
[actionAttribute]: action,
'data-fv-folder-id': String($row.attr('data-fv-folder-id') || '')
});
return $target;
};
const decorate = ($row, folderId) => {
if (!$row || !$row.length) {
return;
}
const id = String(folderId || '').trim();
if (!id) {
return;
}
$row.attr('data-fv-folder-id', id);
decorateTarget($row, '.folder-hand', 'context', true);
decorateTarget($row, '.folder-appname', 'edit', true);
decorateTarget($row, '.folder-dropdown', 'toggle', true).attr('type', 'button');
};
const dispatch = (element, event) => {
const action = String(element?.getAttribute?.(actionAttribute) || '').trim();
const id = String(element?.getAttribute?.('data-fv-folder-id') || '').trim();
const handler = handlers[action];
if (!action || !id || typeof handler !== 'function') {
return false;
}
event?.preventDefault?.();
handler(id, event);
return true;
};
const bind = () => {
jq(doc)
.off(eventName, selector)
.on(eventName, selector, function(event) {
dispatch(this, event);
});
};
const destroy = () => {
jq(doc).off(eventName, selector);
};

return Object.freeze({
bind,
decorate,
destroy,
dispatch,
actionAttribute,
selector
});
};

return Object.freeze({
createController
});
}));
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,12 @@ if (!runtimeFolderOrdering || typeof runtimeFolderOrdering.createOrderCursor !==
} else {
setVmFatalBannerModuleStatus('runtime.folder-ordering.js', 'ok', 'folder ordering contract ready');
}
if (typeof runtimeShared.createFolderRowActionsController !== 'function') {
vmBootstrapMissingModules.push('folder.runtime.row-actions.js');
setVmFatalBannerModuleStatus('folder.runtime.row-actions.js', 'missing', 'folder row action lifecycle unavailable');
} else {
setVmFatalBannerModuleStatus('folder.runtime.row-actions.js', 'ok', 'folder row actions ready');
}
if (
window.FolderViewPlusVmRuntimeLifecycleModuleLoaded !== true
|| !vmLifecycleModule
Expand Down Expand Up @@ -1917,6 +1923,8 @@ const createFolder = (folder, id, position, order, vmInfo, foldersDone, matchCac
} else {
$('#kvm_list > tr.sortable').eq(position - 1).next().after($(fld));
}
const $createdFolderRow = $(`tr.folder-id-${id}`).first();
vmFolderRowActionsController.decorate($createdFolderRow, id);
const safeDepth = Math.max(0, Math.min(8, Number(depthLevel) || 0));
const depthIndentPx = safeDepth * 20;
$(`tr.folder-id-${id}`)
Expand Down Expand Up @@ -2261,6 +2269,18 @@ const dropDownButton = (id, persistState = true) => {
scheduleVmZebraRefresh();
folderEvents.dispatchEvent(new CustomEvent('vm-post-folder-expansion', {detail: { id }}));
};
const vmFolderRowActionsController = runtimeShared.createFolderRowActionsController({
document,
$,
namespace: 'fvVmFolderRowAction',
actionAttribute: 'data-fv-vm-folder-action',
handlers: {
toggle: (id) => dropDownButton(id),
edit: (id) => editFolder(id),
context: (id) => addVMFolderContext(id)
}
});
const bindVmFolderRowActions = () => vmFolderRowActionsController.bind();

const readVmFolderContainerNames = (containers) => {
if (Array.isArray(containers)) {
Expand Down Expand Up @@ -3927,6 +3947,7 @@ window.addVMFolderContext = addVMFolderContext;
window.dropDownButton = dropDownButton;
window.editFolder = editFolder;
window.createFolderBtn = createFolderBtn;
bindVmFolderRowActions();


$.ajaxPrefilter((options, originalOptions, jqXHR) => {
Expand Down Expand Up @@ -3974,6 +3995,7 @@ window.addEventListener('pagehide', () => {
clearTimeout(queuedLoadlistTimer);
clearTimeout(vmRuntimeWidthReflowTimer);
clearTimeout(vmZebraRefreshTimer);
vmFolderRowActionsController.destroy();
vmNativeDetailRowObserver?.disconnect?.();
if (vmNativeToggleClickHost instanceof HTMLTableElement) {
vmNativeToggleClickHost.removeEventListener('click', handleVmNativeToggleClick, true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,12 @@ function fvplus_runtime_integrity_should_track(string $relativePath): bool
return (bool)preg_match('/\.(?:php|js|sh|css|json)$/i', $relativePath);
}

function fvplus_runtime_integrity_is_managed_runtime_file(string $relativePath): bool
{
$relativePath = ltrim(str_replace('\\', '/', $relativePath), '/');
return $relativePath === 'icon-asset-pack.json';
}

function fvplus_get_runtime_integrity_snapshot(string $privacyMode = 'sanitized'): array
{
global $sourceDir;
Expand Down Expand Up @@ -582,7 +588,11 @@ function fvplus_get_runtime_integrity_snapshot(string $privacyMode = 'sanitized'
}
}
foreach (array_values(array_unique($candidates)) as $relative) {
if (fvplus_runtime_integrity_should_track($relative) && !isset($expected[$relative])) {
if (
fvplus_runtime_integrity_should_track($relative)
&& !fvplus_runtime_integrity_is_managed_runtime_file($relative)
&& !isset($expected[$relative])
) {
$unexpectedCount++;
$findings[] = ['kind' => 'unexpected', 'path' => $relative];
}
Expand Down
5 changes: 5 additions & 0 deletions tests/deprecation-registry.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@ test('deprecation registry enforces compatibility and removed-token contracts',
test('removed native organizer integration leaves only bounded legacy browser-state cleanup', () => {
const pluginRoot = path.join(repoRoot, 'src/folderview.plus/usr/local/emhttp/plugins/folderview.plus');
const settingsRuntime = fs.readFileSync(path.join(pluginRoot, 'scripts/folderviewplus.js'), 'utf8');
const pluginManifest = fs.readFileSync(path.join(repoRoot, 'folderview.plus.plg'), 'utf8');
assert.equal(fs.existsSync(path.join(pluginRoot, 'scripts/folderviewplus.native-organizer.js')), false);
assert.match(settingsRuntime, /const REMOVED_SETTINGS_STORAGE_KEYS = Object\.freeze\(\[\s*'fv\.native\.organizer\.status\.v1'\s*\]\);/);
assert.match(settingsRuntime, /removeSettingsStorage\(storageKey, \{ delayMs: 0 \}\);/);
assert.match(
pluginManifest,
/rm -f "&plugdir;\/scripts\/folderviewplus\.native-organizer\.js" "&plugdir;\/scripts\/docker\.member-menu\.js"/
);
});
Loading