From 5ccd0b27a6451678c569204278c5c9a28771f32b Mon Sep 17 00:00:00 2001 From: chodeus Date: Sun, 9 Aug 2026 09:18:08 +0800 Subject: [PATCH 1/2] fix: match autostart membership to what the UI renders MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit syncContainerOrder decided folder membership differently from docker.js, so the autostart file could disagree with the screen. Docker labels were missing entirely. docker.js assigns a container to a folder when its `folder.view3` label matches the folder name, and treats that claim as beating another folder's regex. The PHP had no label handling, so a container assigned purely by label rendered inside its folder but was invisible here — it was ordered as an unassigned container instead of with its group. Labels are read from getDockerJSON("/containers/json?all=1"), the same endpoint readInfo already uses, because getDockerContainers() carries no Labels key. The regex gate diverged in both directions. empty("0") is true in PHP, so a regex of "0" was dropped while docker.js applied it; conversely a whitespace regex was applied here while docker.js skips it via trim(). Gate on trim($regex) !== '' to mirror the JS test. Verified on Unraid 7.3.2: a container labelled for "Utilities" moves from line 1 (unrecognised, prepended) to directly after that folder's block. Output is byte-identical for a config using neither labels nor regex. --- .../plugins/folder.view3/server/lib.php | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/src/folder.view3/usr/local/emhttp/plugins/folder.view3/server/lib.php b/src/folder.view3/usr/local/emhttp/plugins/folder.view3/server/lib.php index 2e82dcdb..fe9502a0 100644 --- a/src/folder.view3/usr/local/emhttp/plugins/folder.view3/server/lib.php +++ b/src/folder.view3/usr/local/emhttp/plugins/folder.view3/server/lib.php @@ -314,17 +314,38 @@ function syncContainerOrder(string $type): void { return; } + // `folder.view3: ` label claims, keyed by container name. getDockerContainers() + // carries no Labels, so read them from the same raw endpoint readInfo() uses. + $ctLabels = []; + $rawCts = $dockerClient->getDockerJSON("/containers/json?all=1"); + if (is_array($rawCts)) { + foreach ($rawCts as $rc) { + $rcName = ltrim($rc['Names'][0] ?? '', '/'); + $rcLabel = $rc['Labels']['folder.view3'] ?? ''; + if ($rcName !== '' && $rcLabel !== '') { $ctLabels[$rcName] = $rcLabel; } + } + } + $folderNameSet = []; + foreach ($folders as $folder) { + if (isset($folder['name'])) { $folderNameSet[$folder['name']] = true; } + } + $folderContainers = []; $folderNames = []; $assignedContainers = []; - // Explicit members of any folder beat regex matches elsewhere (issue #46) + // Explicit members and label claims of any folder beat regex matches elsewhere (issue #46) $explicitAssigned = []; foreach ($folders as $folder) { $explicitAssigned = array_merge($explicitAssigned, $folder['containers'] ?? []); } + foreach ($ctLabels as $ctName => $ctLabel) { + if (isset($folderNameSet[$ctLabel])) { $explicitAssigned[] = $ctName; } + } foreach ($folders as $folderId => $folder) { $members = $folder['containers'] ?? []; - if (!empty($folder['regex'])) { + // trim(), not empty(): empty("0") is true in PHP, so a regex of "0" was silently + // dropped here while docker.js applied it — autostart then disagreed with the screen. + if (isset($folder['regex']) && trim($folder['regex']) !== '') { $regex = '/' . str_replace('/', '\/', $folder['regex']) . '/'; foreach ($allContainerNames as $name) { if (@preg_match($regex, $name) && !in_array($name, $members) && !in_array($name, $explicitAssigned)) { @@ -332,6 +353,11 @@ function syncContainerOrder(string $type): void { } } } + foreach ($ctLabels as $ctName => $ctLabel) { + if ($ctLabel === ($folder['name'] ?? null) && !in_array($ctName, $members)) { + $members[] = $ctName; + } + } $members = array_values(array_filter($members, function($m) use ($allContainerNames, $assignedContainers) { return in_array($m, $allContainerNames) && !in_array($m, $assignedContainers); })); From 2023d6cf61ca0772c0f248ee44a99c9f8afd5b6a Mon Sep 17 00:00:00 2001 From: chodeus Date: Sun, 9 Aug 2026 20:55:38 +0800 Subject: [PATCH 2/2] fix: fail closed on label read, and type-check the regex MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two defects in the previous commit, both raised in review. The label read failed open: a failed or partial Docker response left $ctLabels empty and the order was still computed and written, silently emitting label-assigned containers as unassigned. Abort before any write instead, matching the $ctListComplete guard directly above it (#214). The regex gate called trim() on an unvalidated value. empty() tolerated any type, trim() raises a TypeError on PHP 8, so a folder whose regex is not a string (a hand-edited or imported config) killed the sync outright — verified as exit 255 against a folder with an array regex. Check is_string() first, which also matches docker.js's `typeof folder.regex === 'string'`. Verified on Unraid 7.3.2: the guard does not false-trigger (raw and container counts both 45) and the sync still reaches the write; an array regex now completes instead of fataling; a container labelled for Utilities is still placed with that folder's block. --- .../plugins/folder.view3/server/lib.php | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/src/folder.view3/usr/local/emhttp/plugins/folder.view3/server/lib.php b/src/folder.view3/usr/local/emhttp/plugins/folder.view3/server/lib.php index fe9502a0..9417ca4f 100644 --- a/src/folder.view3/usr/local/emhttp/plugins/folder.view3/server/lib.php +++ b/src/folder.view3/usr/local/emhttp/plugins/folder.view3/server/lib.php @@ -318,12 +318,21 @@ function syncContainerOrder(string $type): void { // carries no Labels, so read them from the same raw endpoint readInfo() uses. $ctLabels = []; $rawCts = $dockerClient->getDockerJSON("/containers/json?all=1"); - if (is_array($rawCts)) { - foreach ($rawCts as $rc) { - $rcName = ltrim($rc['Names'][0] ?? '', '/'); - $rcLabel = $rc['Labels']['folder.view3'] ?? ''; - if ($rcName !== '' && $rcLabel !== '') { $ctLabels[$rcName] = $rcLabel; } + // Fail closed. A failed or partial read yields no label claims, and the order below would + // then write label-assigned containers back out as unassigned — the same hazard $ctListComplete + // guards against above, so abort rather than fall through to the permissive path. + if (!is_array($rawCts) || count($rawCts) < count($allContainerNames)) { + fv3_debug_log("syncContainerOrder: label read unavailable or incomplete, aborting before write"); + return; + } + foreach ($rawCts as $rc) { + $rcName = is_array($rc) ? ltrim($rc['Names'][0] ?? '', '/') : ''; + if ($rcName === '') { + fv3_debug_log("syncContainerOrder: unnamed container in label read, aborting before write"); + return; } + $rcLabel = $rc['Labels']['folder.view3'] ?? ''; + if (is_string($rcLabel) && $rcLabel !== '') { $ctLabels[$rcName] = $rcLabel; } } $folderNameSet = []; foreach ($folders as $folder) { @@ -343,9 +352,10 @@ function syncContainerOrder(string $type): void { } foreach ($folders as $folderId => $folder) { $members = $folder['containers'] ?? []; - // trim(), not empty(): empty("0") is true in PHP, so a regex of "0" was silently - // dropped here while docker.js applied it — autostart then disagreed with the screen. - if (isset($folder['regex']) && trim($folder['regex']) !== '') { + // is_string + trim, not empty(): empty("0") is true in PHP, so a regex of "0" was + // silently dropped while docker.js applied it. The type check mirrors docker.js and + // keeps a non-string regex from fataling trim() (TypeError on PHP 8). + if (is_string($folder['regex'] ?? null) && trim($folder['regex']) !== '') { $regex = '/' . str_replace('/', '\/', $folder['regex']) . '/'; foreach ($allContainerNames as $name) { if (@preg_match($regex, $name) && !in_array($name, $members) && !in_array($name, $explicitAssigned)) {