Skip to content

Commit bb24c51

Browse files
author
FolderView Plus Test
committed
Fix docker force update sync and expand support bundle runtime details
1 parent 8743f30 commit bb24c51

4 files changed

Lines changed: 210 additions & 3 deletions

File tree

src/folderview.plus/usr/local/emhttp/plugins/folderview.plus/scripts/docker.runtime.preview-actions.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -234,13 +234,13 @@
234234
}
235235
};
236236

237-
const findDockerFolderStorageRow = (id, containerName) => {
237+
const findDockerFolderMemberRow = (id, containerName) => {
238238
const folderId = String(id || '').trim();
239239
const safeContainerName = String(containerName || '').trim();
240240
if (!folderId || !safeContainerName) {
241241
return jq();
242242
}
243-
const $rows = jq(`tr.folder-id-${folderId} div.folder-storage > tr`);
243+
const $rows = jq(`tr.folder-id-${folderId} div.folder-storage > tr, tr.folder-${folderId}-element`);
244244
return $rows.filter((_, row) => {
245245
const rowId = String(row?.id || '').trim();
246246
if (rowId === `ct-${safeContainerName}`) {
@@ -297,7 +297,7 @@
297297
if (!containerName) {
298298
return;
299299
}
300-
const $row = findDockerFolderStorageRow(id, containerName);
300+
const $row = findDockerFolderMemberRow(id, containerName);
301301
syncDockerStorageRowStatus($row, entry);
302302
syncDockerStorageRowUpdateColumn($row, entry);
303303
});

src/folderview.plus/usr/local/emhttp/plugins/folderview.plus/server/lib.diagnostics.php

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1452,6 +1452,46 @@ function diagnosticsBuildStateSnapshot(string $type, array $folders, array $pref
14521452
];
14531453
}
14541454

1455+
$entityDetails = [];
1456+
$entityDetailsTotal = 0;
1457+
$entityDetailsMaxEntries = 200;
1458+
$managerCounts = [];
1459+
foreach ($validNames as $name) {
1460+
$item = $infoByName[$name] ?? null;
1461+
if (!is_array($item)) {
1462+
continue;
1463+
}
1464+
$entityDetailsTotal++;
1465+
$kind = $type === 'docker' ? diagnosticsStateKindForDockerItem($item) : diagnosticsStateKindForVmItem($item);
1466+
$manager = '';
1467+
$updated = null;
1468+
if ($type === 'docker') {
1469+
$manager = trim((string)($item['info']['State']['manager'] ?? ($item['manager'] ?? '')));
1470+
$updated = $item['info']['State']['Updated'] ?? ($item['Updated'] ?? null);
1471+
$managerKey = $manager !== '' ? $manager : 'unclassified';
1472+
$managerCounts[$managerKey] = (int)($managerCounts[$managerKey] ?? 0) + 1;
1473+
}
1474+
if (!is_bool($updated)) {
1475+
$updated = null;
1476+
}
1477+
if ($entityDetailsTotal > $entityDetailsMaxEntries) {
1478+
continue;
1479+
}
1480+
$entityDetails[] = [
1481+
'name' => normalizeDiagnosticsPrivacyMode($privacyMode) === 'full' ? (string)$name : null,
1482+
'nameHash' => diagnosticsHashShort((string)$name),
1483+
'state' => $kind,
1484+
'assigned' => isset($assignedItemSet[$name]),
1485+
'manager' => $manager !== '' ? $manager : null,
1486+
'managed' => $manager === 'dockerman',
1487+
'updated' => $updated,
1488+
'updateState' => $updated === true ? 'upToDate' : ($updated === false ? 'available' : 'unknown')
1489+
];
1490+
}
1491+
if (!empty($managerCounts)) {
1492+
ksort($managerCounts);
1493+
}
1494+
14551495
$totalItems = count($validNames);
14561496
$assignedItems = count($assignedItemSet);
14571497
$unassignedItems = max(0, $totalItems - $assignedItems);
@@ -1465,6 +1505,13 @@ function diagnosticsBuildStateSnapshot(string $type, array $folders, array $pref
14651505
'nestedFolderCount' => $nestedFolderCount,
14661506
'maxDepth' => $maxDepth,
14671507
'updateCounts' => $updateCounts,
1508+
'managerCounts' => $managerCounts,
1509+
'entityDetails' => [
1510+
'total' => $entityDetailsTotal,
1511+
'maxEntries' => $entityDetailsMaxEntries,
1512+
'truncated' => $entityDetailsTotal > count($entityDetails),
1513+
'entries' => $entityDetails
1514+
],
14681515
'summary' => [
14691516
'folderTotalsByStatus' => $folderStatusTotals,
14701517
'memberTotals' => $memberTotals,
@@ -2131,6 +2178,44 @@ function diagnosticsBuildSupportBundlePluginStateSection(array $diagnostics, arr
21312178
return $section;
21322179
}
21332180

2181+
function diagnosticsBuildSupportBundleRuntimeEntityDetails(string $type, array $stateSnapshot, array &$redactor): array {
2182+
$details = is_array($stateSnapshot['entityDetails'] ?? null) ? $stateSnapshot['entityDetails'] : [];
2183+
$entries = [];
2184+
$fieldPath = 'runtimeState.' . $type . '.entityDetails.entries.*';
2185+
foreach (array_values(is_array($details['entries'] ?? null) ? $details['entries'] : []) as $entry) {
2186+
if (!is_array($entry)) {
2187+
continue;
2188+
}
2189+
$name = (string)($entry['name'] ?? '');
2190+
$manager = trim((string)($entry['manager'] ?? ''));
2191+
$updated = array_key_exists('updated', $entry) && is_bool($entry['updated']) ? (bool)$entry['updated'] : null;
2192+
$entries[] = [
2193+
'name' => diagnosticsSupportBundleRedactScalar($redactor, $fieldPath . '.name', $name),
2194+
'nameHash' => diagnosticsSupportBundleHashValue($redactor, $fieldPath . '.nameHash', $name),
2195+
'state' => in_array((string)($entry['state'] ?? ''), ['started', 'paused', 'stopped'], true)
2196+
? (string)$entry['state']
2197+
: 'stopped',
2198+
'assigned' => (bool)($entry['assigned'] ?? false),
2199+
'manager' => $manager !== '' ? $manager : null,
2200+
'managed' => (bool)($entry['managed'] ?? false),
2201+
'updated' => $updated,
2202+
'updateState' => in_array((string)($entry['updateState'] ?? ''), ['available', 'upToDate', 'unknown'], true)
2203+
? (string)$entry['updateState']
2204+
: 'unknown'
2205+
];
2206+
}
2207+
if ((bool)($details['truncated'] ?? false)) {
2208+
diagnosticsSupportBundleMarkRedaction($redactor, 'truncatedFields', 'runtimeState.' . $type . '.entityDetails.entries');
2209+
}
2210+
return [
2211+
'total' => (int)($details['total'] ?? count($entries)),
2212+
'maxEntries' => (int)($details['maxEntries'] ?? count($entries)),
2213+
'truncated' => (bool)($details['truncated'] ?? false),
2214+
'managerCounts' => is_array($stateSnapshot['managerCounts'] ?? null) ? $stateSnapshot['managerCounts'] : [],
2215+
'entries' => $entries
2216+
];
2217+
}
2218+
21342219
function diagnosticsBuildSupportBundleRuntimeTypeSection(string $type, array $typeData, array &$redactor): array {
21352220
$stateSnapshot = is_array($typeData['stateSnapshot'] ?? null) ? $typeData['stateSnapshot'] : [];
21362221
$folders = [];
@@ -2182,6 +2267,7 @@ static function ($name) use (&$redactor, $fieldPath): ?string {
21822267
'maxDepth' => (int)($stateSnapshot['maxDepth'] ?? 0),
21832268
'folders' => $folders
21842269
],
2270+
'entityDetails' => diagnosticsBuildSupportBundleRuntimeEntityDetails($type, $stateSnapshot, $redactor),
21852271
'updateStateSummary' => is_array($stateSnapshot['updateCounts'] ?? null) ? $stateSnapshot['updateCounts'] : [],
21862272
'preflight' => [
21872273
'foldersPath' => diagnosticsSupportBundleRedactScalar($redactor, 'runtimeState.' . $type . '.preflight.foldersPath', $foldersPath, true),

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,3 +129,31 @@ test('docker runtime sync normalizes hidden member rows before expand', () => {
129129
assert.match(dockerJs, /folder\.runtimeContainers = runtimeContainers;\s*syncDockerFolderMemberRows\(id,\s*runtimeContainers\);/s);
130130
assert.match(dockerJs, /folder\.containers = newFolder;[\s\S]*syncDockerFolderMemberRows\(id,\s*newFolder\);/s);
131131
});
132+
133+
test('docker runtime sync rewrites both hidden and expanded member rows', () => {
134+
assert.match(dockerRuntimeInfoJs, /const readDockerHostRowUpdatedState = \(name\) => \{/);
135+
assert.match(dockerPreviewActionsModule.createApi({
136+
window: {},
137+
$: Object.assign(() => ({}), {
138+
i18n: (key) => key
139+
}),
140+
escapeHtml: (value) => String(value ?? '')
141+
}).buildDockerMemberUpdateColumnHtml({ name: 'demo', manager: 'dockerman', update: false }), /force-update/);
142+
assert.match(
143+
dockerPreviewActionsModule.createApi({
144+
window: {},
145+
$: Object.assign(() => ({}), {
146+
i18n: (key) => key
147+
}),
148+
escapeHtml: (value) => String(value ?? '')
149+
}).syncDockerFolderMemberRows.toString(),
150+
/findDockerFolderMemberRow/
151+
);
152+
assert.match(dockerPreviewActionsModule.createApi({
153+
window: {},
154+
$: Object.assign(() => ({}), {
155+
i18n: (key) => key
156+
}),
157+
escapeHtml: (value) => String(value ?? '')
158+
}).syncDockerFolderMemberRows.toString(), /tr\.folder-id-\$\{folderId\} div\.folder-storage > tr, tr\.folder-\$\{folderId\}-element/);
159+
});

tests/support-bundle-v2-contract.test.mjs

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,41 @@ $diagnostics = [
276276
'nestedFolderCount' => 1,
277277
'maxDepth' => 1,
278278
'updateCounts' => ['available' => 1, 'upToDate' => 1, 'unknown' => 1, 'total' => 3],
279+
'managerCounts' => ['composeman' => 1, 'dockerman' => 1, 'unclassified' => 1],
280+
'entityDetails' => [
281+
'total' => 3,
282+
'maxEntries' => 200,
283+
'truncated' => false,
284+
'entries' => [
285+
[
286+
'name' => 'PlexMediaServer',
287+
'state' => 'started',
288+
'assigned' => true,
289+
'manager' => 'dockerman',
290+
'managed' => true,
291+
'updated' => false,
292+
'updateState' => 'available'
293+
],
294+
[
295+
'name' => 'SonarrStack',
296+
'state' => 'started',
297+
'assigned' => true,
298+
'manager' => 'composeman',
299+
'managed' => false,
300+
'updated' => null,
301+
'updateState' => 'unknown'
302+
],
303+
[
304+
'name' => 'LegacyTool',
305+
'state' => 'stopped',
306+
'assigned' => false,
307+
'manager' => null,
308+
'managed' => false,
309+
'updated' => true,
310+
'updateState' => 'upToDate'
311+
]
312+
]
313+
],
279314
'folders' => [
280315
[
281316
'folderId' => 'root01',
@@ -359,6 +394,23 @@ $diagnostics = [
359394
'nestedFolderCount' => 0,
360395
'maxDepth' => 0,
361396
'updateCounts' => ['available' => 0, 'upToDate' => 0, 'unknown' => 1, 'total' => 1],
397+
'managerCounts' => [],
398+
'entityDetails' => [
399+
'total' => 1,
400+
'maxEntries' => 200,
401+
'truncated' => false,
402+
'entries' => [
403+
[
404+
'name' => 'Orion VM Secret',
405+
'state' => 'stopped',
406+
'assigned' => false,
407+
'manager' => null,
408+
'managed' => false,
409+
'updated' => null,
410+
'updateState' => 'unknown'
411+
]
412+
]
413+
],
362414
'folders' => []
363415
]
364416
]
@@ -515,6 +567,25 @@ test('support bundle v2 fixture exposes the exact top-level contract', () => {
515567
assert.equal(bundle.runtimeState.docker.folderHierarchySummary.folders[0].parentId, '');
516568
assert.equal(bundle.runtimeState.docker.folderHierarchySummary.folders[0].depth, 0);
517569
assert.equal(bundle.runtimeState.docker.folderHierarchySummary.folders[1].depth, 1);
570+
assert.equal(bundle.runtimeState.docker.entityDetails.total, 3);
571+
assert.equal(bundle.runtimeState.docker.entityDetails.maxEntries, 200);
572+
assert.equal(bundle.runtimeState.docker.entityDetails.truncated, false);
573+
assert.equal(bundle.runtimeState.docker.entityDetails.managerCounts.dockerman, 1);
574+
assert.equal(bundle.runtimeState.docker.entityDetails.managerCounts.composeman, 1);
575+
assert.equal(bundle.runtimeState.docker.entityDetails.managerCounts.unclassified, 1);
576+
assert.equal(bundle.runtimeState.docker.entityDetails.entries[0].state, 'started');
577+
assert.equal(bundle.runtimeState.docker.entityDetails.entries[0].assigned, true);
578+
assert.equal(bundle.runtimeState.docker.entityDetails.entries[0].manager, 'dockerman');
579+
assert.equal(bundle.runtimeState.docker.entityDetails.entries[0].managed, true);
580+
assert.equal(bundle.runtimeState.docker.entityDetails.entries[0].updated, false);
581+
assert.equal(bundle.runtimeState.docker.entityDetails.entries[0].updateState, 'available');
582+
assert.equal(bundle.runtimeState.vm.entityDetails.total, 1);
583+
assert.deepEqual(bundle.runtimeState.vm.entityDetails.managerCounts, []);
584+
assert.equal(bundle.runtimeState.vm.entityDetails.entries[0].state, 'stopped');
585+
assert.equal(bundle.runtimeState.vm.entityDetails.entries[0].assigned, false);
586+
assert.equal(bundle.runtimeState.vm.entityDetails.entries[0].manager, null);
587+
assert.equal(bundle.runtimeState.vm.entityDetails.entries[0].updated, null);
588+
assert.equal(bundle.runtimeState.vm.entityDetails.entries[0].updateState, 'unknown');
518589
assert.equal(bundle.runtimeState.docker.updateStateSummary.available, 1);
519590
assert.equal(bundle.runtimeState.docker.updateStateSummary.total, 3);
520591
assert.equal(bundle.runtimeState.vm.updateStateSummary.unknown, 1);
@@ -525,6 +596,9 @@ test('support bundle v2 fixture exposes the exact top-level contract', () => {
525596
assert.equal(bundle.runtimeState.docker.folderHierarchySummary.folders[0].folderId, 'root01');
526597
assert.equal(bundle.runtimeState.docker.folderHierarchySummary.folders[1].folderId, 'child01');
527598
assert.equal(bundle.runtimeState.docker.folderHierarchySummary.folders[1].parentId, 'root01');
599+
assert.equal(bundle.runtimeState.docker.entityDetails.entries[0].name, 'PlexMediaServer');
600+
assert.match(bundle.runtimeState.docker.entityDetails.entries[0].nameHash, /^[0-9a-f]{12}$/);
601+
assert.equal(bundle.runtimeState.vm.entityDetails.entries[0].name, 'Orion VM Secret');
528602
} else {
529603
const expandedState = bundle.pluginState.docker.prefs.expandedFolderState || {};
530604
const expandedKeys = Object.keys(expandedState);
@@ -545,6 +619,9 @@ test('support bundle v2 fixture exposes the exact top-level contract', () => {
545619
bundle.runtimeState.docker.folderHierarchySummary.folders[1].parentId,
546620
bundle.runtimeState.docker.folderHierarchySummary.folders[0].folderId
547621
);
622+
assert.equal(bundle.runtimeState.docker.entityDetails.entries[0].name, null);
623+
assert.match(bundle.runtimeState.docker.entityDetails.entries[0].nameHash, /^[0-9a-f]{16}$/);
624+
assert.equal(bundle.runtimeState.vm.entityDetails.entries[0].name, null);
548625
}
549626
assert.equal(Object.prototype.hasOwnProperty.call(bundle, 'diagnostics'), false);
550627
assert.equal(Object.prototype.hasOwnProperty.call(bundle, 'clientTelemetry'), false);
@@ -599,6 +676,10 @@ test('sanitized support bundle fixture redacts paths, names, URLs, IPs, and user
599676
assert.deepEqual(bundle.runtimeState.docker.folderHierarchySummary.folders[0].members.items, []);
600677
assert.equal(bundle.runtimeState.docker.folderHierarchySummary.folders[0].members.itemHashes.length, 2);
601678
assert.match(bundle.runtimeState.docker.folderHierarchySummary.folders[0].members.itemHashes[0], /^[0-9a-f]{16}$/);
679+
assert.equal(bundle.runtimeState.docker.entityDetails.entries[0].name, null);
680+
assert.match(bundle.runtimeState.docker.entityDetails.entries[0].nameHash, /^[0-9a-f]{16}$/);
681+
assert.equal(bundle.runtimeState.docker.entityDetails.entries[2].manager, null);
682+
assert.equal(bundle.runtimeState.docker.entityDetails.entries[2].updateState, 'upToDate');
602683
assert.equal(bundle.healthAndHistory.integrityFindings.docker.duplicateFolderNames.examples[0].name, null);
603684
assert.match(bundle.healthAndHistory.integrityFindings.docker.duplicateFolderNames.examples[0].nameHash, /^[0-9a-f]{16}$/);
604685
assert.deepEqual(bundle.healthAndHistory.integrityFindings.docker.orphanedMembers.folders[0].items, []);
@@ -629,6 +710,7 @@ test('sanitized support bundle fixture redacts paths, names, URLs, IPs, and user
629710
assert.equal(bundle.redactionManifest.hashedFields.includes('system.request.userAgentHash'), true);
630711
assert.equal(bundle.redactionManifest.hashedFields.includes('pluginState.docker.prefs.expandedFolderState.*'), true);
631712
assert.equal(bundle.redactionManifest.hashedFields.includes('runtimeState.docker.folderHierarchySummary.folders.*.folderId'), true);
713+
assert.equal(bundle.redactionManifest.hashedFields.includes('runtimeState.docker.entityDetails.entries.*.nameHash'), true);
632714
assert.equal(bundle.redactionManifest.hashedFields.includes('healthAndHistory.integrityFindings.docker.orphanedMembers.folders.*.folderId'), true);
633715
assert.equal(bundle.redactionManifest.hashedFields.includes('healthAndHistory.recentActions.*.targetHash'), true);
634716
assert.equal(bundle.redactionManifest.hashedFields.includes('healthAndHistory.serverLogTail.pathHash'), true);
@@ -670,6 +752,11 @@ test('full support bundle fixture keeps raw troubleshooting fields and disables
670752
assert.equal(bundle.runtimeState.docker.folderHierarchySummary.folders[1].parentId, 'root01');
671753
assert.equal(bundle.runtimeState.docker.folderHierarchySummary.folders[0].folderName, 'Plex Root Secret');
672754
assert.deepEqual(bundle.runtimeState.docker.folderHierarchySummary.folders[0].members.items, ['PlexMediaServer', 'SonarrStack']);
755+
assert.equal(bundle.runtimeState.docker.entityDetails.entries[0].name, 'PlexMediaServer');
756+
assert.equal(bundle.runtimeState.docker.entityDetails.entries[0].manager, 'dockerman');
757+
assert.equal(bundle.runtimeState.docker.entityDetails.entries[0].updated, false);
758+
assert.equal(bundle.runtimeState.docker.entityDetails.entries[2].manager, null);
759+
assert.equal(bundle.runtimeState.docker.entityDetails.entries[2].updateState, 'upToDate');
673760
assert.equal(bundle.healthAndHistory.integrityFindings.docker.duplicateFolderNames.examples[0].name, 'Plex Root Secret');
674761
assert.deepEqual(bundle.healthAndHistory.integrityFindings.docker.orphanedMembers.folders[0].items, ['PlexMediaServer']);
675762
assert.equal(bundle.healthAndHistory.recentTimeline[0].summary, 'name=PlexMediaServer, folderId=root01, itemCount=2');
@@ -697,4 +784,10 @@ test('vm state snapshot marks all entities as unknown for update totals', () =>
697784
assert.equal(fixture.vmStateSnapshot.updateCounts.upToDate, 0);
698785
assert.equal(fixture.vmStateSnapshot.updateCounts.unknown, 1);
699786
assert.equal(fixture.vmStateSnapshot.updateCounts.total, 1);
787+
assert.equal(fixture.vmStateSnapshot.entityDetails.total, 1);
788+
assert.equal(fixture.vmStateSnapshot.entityDetails.truncated, false);
789+
assert.equal(fixture.vmStateSnapshot.entityDetails.entries[0].name, null);
790+
assert.equal(fixture.vmStateSnapshot.entityDetails.entries[0].state, 'stopped');
791+
assert.equal(fixture.vmStateSnapshot.entityDetails.entries[0].managed, false);
792+
assert.equal(fixture.vmStateSnapshot.entityDetails.entries[0].updateState, 'unknown');
700793
});

0 commit comments

Comments
 (0)