Skip to content

Commit 939ac77

Browse files
Fix support bundle archive URL and asset version telemetry
1 parent 7ba656f commit 939ac77

11 files changed

Lines changed: 145 additions & 20 deletions

archive/folderview.plus-2026.04.02.10.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+
10b92eac243d794313ad6bd81400f5eab05ddc9f3515e20b9e4f6eb17c305a18 folderview.plus-2026.04.04.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.04.04.18">
10-
<!ENTITY md5 "f70091149cd0f7869613fd67ad1adfed">
9+
<!ENTITY version "2026.04.04.19">
10+
<!ENTITY md5 "3bfb19f8e3fd0a49630e6422ab34382b">
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.04.19
17+
- UX: Settings workspace layout, section flows, and table behavior.
18+
- Fix: Server endpoints, runtime payloads, and persistence or validation paths.
19+
- Quality: Release automation, CI smoke coverage, and packaging guards.
20+
21+
1622
###2026.04.04.18
1723
- Feature: Support bundle v2 now exports exact build identity, packaged manifest checksums, source commit metadata, loaded plugin assets, recent plugin actions, plugin-scoped API error log tails, and browser-side FolderView Plus error snapshots.
1824
- Security: Sanitized support bundles redact the new browser and server troubleshooting fields through the shared redaction pipeline while preserving correlation via per-export hashed values.

scripts/perf_baseline.json

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"version": 1,
3-
"generatedAt": "2026-04-04T23:31:35.457Z",
3+
"generatedAt": "2026-04-04T23:44:04.689Z",
44
"notes": "Baseline for perf ratchet guard. Update intentionally when expected growth is accepted.",
55
"assets": {
66
"scripts/folderviewplus.runtime-parity.js": {
@@ -28,12 +28,12 @@
2828
"gzipBytes": 2629
2929
},
3030
"scripts/folderviewplus.support-bundle-browser.js": {
31-
"bytes": 6837,
32-
"gzipBytes": 1805
31+
"bytes": 7746,
32+
"gzipBytes": 1976
3333
},
3434
"scripts/folderviewplus.support-bundle-telemetry.js": {
35-
"bytes": 13422,
36-
"gzipBytes": 2942
35+
"bytes": 13510,
36+
"gzipBytes": 2968
3737
},
3838
"scripts/folderviewplus.activity-diagnostics.js": {
3939
"bytes": 68184,
@@ -109,11 +109,11 @@
109109
}
110110
},
111111
"totals": {
112-
"totalJs": 2467043,
112+
"totalJs": 2468040,
113113
"totalCss": 378650,
114-
"totalJsGzip": 477866,
114+
"totalJsGzip": 478063,
115115
"totalCssGzip": 50712,
116-
"settingsRuntimeJs": 1022348,
117-
"settingsRuntimeJsGzip": 193213
116+
"settingsRuntimeJs": 1023345,
117+
"settingsRuntimeJsGzip": 193410
118118
}
119119
}

src/folderview.plus/usr/local/emhttp/plugins/folderview.plus/scripts/folderviewplus.support-bundle-browser.js

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,14 @@
2525
? deps.storageKeys
2626
: {};
2727

28+
const normalizeAssetVersionToken = (value) => {
29+
const raw = String(value || '').trim();
30+
if (!raw || raw === '0' || raw === 'null' || raw === 'undefined' || raw === 'false') {
31+
return '';
32+
}
33+
return raw;
34+
};
35+
2836
const collectBrowserCapabilities = () => ({
2937
clipboardWrite: Boolean(root?.navigator?.clipboard && typeof root.navigator.clipboard.writeText === 'function'),
3038
cookieEnabled: root?.navigator?.cookieEnabled !== false,
@@ -58,35 +66,47 @@
5866
};
5967
};
6068

61-
const collectLoadedAssetTelemetry = (uiRedactor) => {
69+
const collectLoadedAssetTelemetry = (uiRedactor, options = {}) => {
6270
const doc = root?.document || null;
6371
if (!doc || typeof doc.querySelectorAll !== 'function') {
6472
return { count: 0, entries: [] };
6573
}
6674
const entries = [];
6775
const seen = new Set();
76+
const fallbackVersionToken = normalizeAssetVersionToken(options?.pluginVersion || '');
6877
doc.querySelectorAll('script[src*="/plugins/folderview.plus/"], link[href*="/plugins/folderview.plus/"]').forEach((node) => {
6978
const rawUrl = String(node?.src || node?.href || '').trim();
7079
if (!rawUrl || seen.has(rawUrl)) {
7180
return;
7281
}
7382
seen.add(rawUrl);
7483
let pathname = rawUrl;
84+
let rawVersionQuery = '';
7585
let versionQuery = '';
7686
let bootQuery = '';
87+
let versionSource = 'none';
7788
try {
7889
const parsed = new URL(rawUrl, root?.location?.origin || 'http://fvplus.local');
7990
pathname = parsed.pathname || rawUrl;
80-
versionQuery = String(parsed.searchParams.get('v') || '');
91+
rawVersionQuery = String(parsed.searchParams.get('v') || '');
92+
versionQuery = normalizeAssetVersionToken(rawVersionQuery);
8193
bootQuery = String(parsed.searchParams.get('boot') || '');
8294
} catch (_error) {
8395
pathname = rawUrl.replace(/^https?:\/\/[^/?#]+/i, '').replace(/[?#].*$/, '') || rawUrl;
8496
}
97+
if (versionQuery) {
98+
versionSource = 'query';
99+
} else if (fallbackVersionToken) {
100+
versionQuery = fallbackVersionToken;
101+
versionSource = 'bundleMeta.pluginVersion';
102+
}
85103
entries.push({
86104
tag: String(node?.tagName || '').toLowerCase() || 'asset',
87105
url: uiRedactor ? uiRedactor.redactUrl(`uiTelemetry.loadedAssets.entries.${entries.length}.url`, rawUrl) : pathname,
88106
path: pathname,
107+
rawVersionQuery,
89108
versionQuery,
109+
versionSource,
90110
bootQuery,
91111
async: node?.async === true,
92112
defer: node?.defer === true,

src/folderview.plus/usr/local/emhttp/plugins/folderview.plus/scripts/folderviewplus.support-bundle-telemetry.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,9 @@
248248
existingUiTelemetry.browserCapabilities = collectBrowserCapabilities();
249249
existingUiTelemetry.clientStorage = collectClientStorageDiagnostics();
250250
existingUiTelemetry.currentPage = collectCurrentPageTelemetry(uiRedactor);
251-
existingUiTelemetry.loadedAssets = collectLoadedAssetTelemetry(uiRedactor);
251+
existingUiTelemetry.loadedAssets = collectLoadedAssetTelemetry(uiRedactor, {
252+
pluginVersion: payload.bundleMeta?.pluginVersion || ''
253+
});
252254
existingUiTelemetry.performance = uiRedactor.sanitizeValue(
253255
'uiTelemetry.performance',
254256
'performance',

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

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1809,9 +1809,18 @@ function diagnosticsResolveSupportBundleManifestMetadata(): array {
18091809
$manifestMetadata['manifestPath'] = basename(str_replace('\\', '/', $manifestPath));
18101810
$manifestMetadata['manifestPathHash'] = diagnosticsHashShort($manifestPath);
18111811
$manifestMetadata['manifestSha256'] = @hash_file('sha256', $manifestPath) ?: null;
1812+
$manifestEntities = [];
18121813
if (preg_match('/<!ENTITY\s+md5\s+"([^"]+)"/i', $contents, $match)) {
18131814
$manifestMetadata['manifestMd5'] = (string)($match[1] ?? '');
18141815
}
1816+
foreach (['name', 'version', 'github', 'pluginURL'] as $entityKey) {
1817+
if (preg_match('/<!ENTITY\s+' . preg_quote($entityKey, '/') . '\s+"([^"]+)"/i', $contents, $match)) {
1818+
$entityValue = html_entity_decode((string)($match[1] ?? ''), ENT_QUOTES | ENT_XML1, 'UTF-8');
1819+
if ($entityValue !== '') {
1820+
$manifestEntities[$entityKey] = $entityValue;
1821+
}
1822+
}
1823+
}
18151824
if (preg_match('/<!ENTITY\s+github\s+"([^"]+)"/i', $contents, $match)) {
18161825
$githubRepo = trim((string)($match[1] ?? ''));
18171826
$manifestMetadata['githubRepository'] = $githubRepo !== '' ? $githubRepo : null;
@@ -1831,6 +1840,18 @@ function diagnosticsResolveSupportBundleManifestMetadata(): array {
18311840
}
18321841
}
18331842
}
1843+
if (!empty($manifestEntities)) {
1844+
foreach (['manifestUrl', 'archiveUrl'] as $urlKey) {
1845+
$urlValue = (string)($manifestMetadata[$urlKey] ?? '');
1846+
if ($urlValue === '') {
1847+
continue;
1848+
}
1849+
foreach ($manifestEntities as $entityKey => $entityValue) {
1850+
$urlValue = str_replace('&' . $entityKey . ';', (string)$entityValue, $urlValue);
1851+
}
1852+
$manifestMetadata[$urlKey] = $urlValue;
1853+
}
1854+
}
18341855
break;
18351856
}
18361857

@@ -1843,8 +1864,16 @@ function diagnosticsBuildSupportBundleBuildIdentitySection(array $diagnostics):
18431864
$sourceCommitSha = trim((string)($buildMetadata['sourceCommitSha'] ?? ''));
18441865
$sourceTreeSha = trim((string)($buildMetadata['sourceTreeSha'] ?? ''));
18451866
$sourceBranch = trim((string)($buildMetadata['sourceBranch'] ?? diagnosticsResolveSupportBundleChannel()));
1846-
$manifestUrl = trim((string)($manifestMetadata['manifestUrl'] ?? ($buildMetadata['manifestUrl'] ?? '')));
1847-
$archiveUrl = trim((string)($manifestMetadata['archiveUrl'] ?? ($buildMetadata['archiveUrl'] ?? '')));
1867+
$buildManifestUrl = trim((string)($buildMetadata['manifestUrl'] ?? ''));
1868+
$buildArchiveUrl = trim((string)($buildMetadata['archiveUrl'] ?? ''));
1869+
$resolvedManifestUrl = trim((string)($manifestMetadata['manifestUrl'] ?? ''));
1870+
$resolvedArchiveUrl = trim((string)($manifestMetadata['archiveUrl'] ?? ''));
1871+
$manifestUrl = ($resolvedManifestUrl !== '' && strpos($resolvedManifestUrl, '&') === false)
1872+
? $resolvedManifestUrl
1873+
: $buildManifestUrl;
1874+
$archiveUrl = ($resolvedArchiveUrl !== '' && strpos($resolvedArchiveUrl, '&') === false)
1875+
? $resolvedArchiveUrl
1876+
: $buildArchiveUrl;
18481877

18491878
return [
18501879
'pluginVersion' => (string)($diagnostics['pluginVersion'] ?? readInstalledVersion()),

tests/settings-surface-regression.test.mjs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,10 @@ test('settings diagnostics exports client perf and theme telemetry helpers', ()
5151
assert.match(supportBundlePreviewJs, /const buildSupportBundlePreviewSectionCards = \(bundle\) =>/);
5252
assert.match(supportBundlePreviewJs, /const buildSupportBundleRedactionPreviewHtml = \(bundle\) =>/);
5353
assert.match(supportBundleBrowserJs, /FolderViewPlusSupportBundleBrowserModuleLoaded = true/);
54-
assert.match(supportBundleBrowserJs, /const collectLoadedAssetTelemetry = \(uiRedactor\) =>/);
54+
assert.match(supportBundleBrowserJs, /const collectLoadedAssetTelemetry = \(uiRedactor, options = \{\}\) =>/);
55+
assert.match(supportBundleBrowserJs, /const fallbackVersionToken = normalizeAssetVersionToken\(options\?\.pluginVersion \|\| ''\);/);
56+
assert.match(supportBundleBrowserJs, /rawVersionQuery,/);
57+
assert.match(supportBundleBrowserJs, /versionSource,/);
5558
assert.match(supportBundleBrowserJs, /const collectBrowserConsoleErrors = \(\) =>/);
5659
assert.match(supportBundleTelemetryJs, /FolderViewPlusSupportBundleTelemetryModuleLoaded = true/);
5760
assert.match(supportBundleTelemetryJs, /const createApi = \(deps = \{\}\) =>/);
@@ -96,7 +99,8 @@ test('settings diagnostics exports client perf and theme telemetry helpers', ()
9699
assert.match(supportBundleTelemetryJs, /existingUiTelemetry\.browserCapabilities = collectBrowserCapabilities\(\);/);
97100
assert.match(supportBundleTelemetryJs, /existingUiTelemetry\.clientStorage = collectClientStorageDiagnostics\(\);/);
98101
assert.match(supportBundleTelemetryJs, /existingUiTelemetry\.currentPage = collectCurrentPageTelemetry\(uiRedactor\);/);
99-
assert.match(supportBundleTelemetryJs, /existingUiTelemetry\.loadedAssets = collectLoadedAssetTelemetry\(uiRedactor\);/);
102+
assert.match(supportBundleTelemetryJs, /existingUiTelemetry\.loadedAssets = collectLoadedAssetTelemetry\(uiRedactor, \{/);
103+
assert.match(supportBundleTelemetryJs, /pluginVersion: payload\.bundleMeta\?\.pluginVersion \|\| ''/);
100104
assert.match(supportBundleTelemetryJs, /existingUiTelemetry\.requestErrors = uiRedactor\.sanitizeValue\(/);
101105
assert.match(supportBundleTelemetryJs, /existingUiTelemetry\.browserConsoleErrors = uiRedactor\.sanitizeValue\(/);
102106
assert.match(supportBundleTelemetryJs, /existingUiTelemetry\.folderEditorDebug = uiRedactor\.sanitizeValue\(/);
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
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+
import vm from 'node:vm';
6+
7+
const repoRoot = path.resolve(process.cwd());
8+
const browserModulePath = path.join(
9+
repoRoot,
10+
'src/folderview.plus/usr/local/emhttp/plugins/folderview.plus/scripts/folderviewplus.support-bundle-browser.js'
11+
);
12+
const browserModuleSource = fs.readFileSync(browserModulePath, 'utf8');
13+
14+
const loadBrowserModule = (root = {}) => {
15+
const context = {
16+
globalThis: root,
17+
module: { exports: {} },
18+
exports: {},
19+
URL,
20+
console
21+
};
22+
vm.runInNewContext(browserModuleSource, context, {
23+
filename: browserModulePath
24+
});
25+
return context.module.exports;
26+
};
27+
28+
test('support bundle browser telemetry resolves useful asset version tokens when host autov returns v=0', () => {
29+
const root = {
30+
location: {
31+
origin: 'https://tower.local'
32+
},
33+
document: {
34+
querySelectorAll() {
35+
return [
36+
{ tagName: 'SCRIPT', src: '/plugins/folderview.plus/scripts/folderviewplus.fatal-banner.js?v=0' },
37+
{ tagName: 'LINK', href: '/plugins/folderview.plus/styles/folderviewplus.css?v=2026.04.04.18', rel: 'stylesheet', sheet: {} },
38+
{ tagName: 'SCRIPT', src: '/plugins/folderview.plus/scripts/folderviewplus.request.js' }
39+
];
40+
}
41+
}
42+
};
43+
const browserModule = loadBrowserModule(root);
44+
const collectors = browserModule.createCollectors();
45+
const loadedAssets = collectors.collectLoadedAssetTelemetry(null, {
46+
pluginVersion: '2026.04.04.18'
47+
});
48+
49+
assert.equal(loadedAssets.count, 3);
50+
51+
assert.equal(loadedAssets.entries[0].rawVersionQuery, '0');
52+
assert.equal(loadedAssets.entries[0].versionQuery, '2026.04.04.18');
53+
assert.equal(loadedAssets.entries[0].versionSource, 'bundleMeta.pluginVersion');
54+
55+
assert.equal(loadedAssets.entries[1].rawVersionQuery, '2026.04.04.18');
56+
assert.equal(loadedAssets.entries[1].versionQuery, '2026.04.04.18');
57+
assert.equal(loadedAssets.entries[1].versionSource, 'query');
58+
59+
assert.equal(loadedAssets.entries[2].rawVersionQuery, '');
60+
assert.equal(loadedAssets.entries[2].versionQuery, '2026.04.04.18');
61+
assert.equal(loadedAssets.entries[2].versionSource, 'bundleMeta.pluginVersion');
62+
});

0 commit comments

Comments
 (0)