Skip to content

Commit fe7d598

Browse files
author
FolderView Plus Test
committed
Extract Docker host guards and diagnostics modules
1 parent 1707b84 commit fe7d598

12 files changed

Lines changed: 963 additions & 656 deletions

archive/folderview.plus-2026.04.08.07.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+
9e2627feec43c8af65312e548026de0feed7756c3e410e008b84dc3f75508273 folderview.plus-2026.04.14.11.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.14.10">
10-
<!ENTITY md5 "4329a3d605aea560f592a8418fda4422">
9+
<!ENTITY version "2026.04.14.11">
10+
<!ENTITY md5 "57d82d7b18a26a8fbedc594d74331df3">
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.14.11
17+
- Fix: Docker support-bundle snapshots, trace storage caps, and rendered-state diagnostics.
18+
- Fix: Docker runtime rows, folder state, and container interactions.
19+
- Fix: Diagnostics surfaces, issue reports, and support bundle coverage.
20+
- Refactor: Shared runtime contracts, request plumbing, and cross-page foundations.
21+
22+
1623
###2026.04.14.10
1724
- Fix: Docker support-bundle snapshots, trace storage caps, and rendered-state diagnostics.
1825
- Quality: Release-note generation, retry cleanup, and packaging guards.

src/folderview.plus/usr/local/emhttp/plugins/folderview.plus/folderview.plus.Docker.page

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ $fvplusRuntimePreflightHasFatal = runtimePreflightHasFatal($fvplusRuntimePreflig
6464
<script src="<?php fvplus_asset('/plugins/folderview.plus/scripts/docker.runtime.preview-actions.js')?>"></script>
6565
<script src="<?php fvplus_asset('/plugins/folderview.plus/scripts/docker.runtime.hierarchy.js')?>"></script>
6666
<script src="<?php fvplus_asset('/plugins/folderview.plus/scripts/docker.runtime.actions.js')?>"></script>
67+
<script src="<?php fvplus_asset('/plugins/folderview.plus/scripts/docker.runtime.host-guards.js')?>"></script>
68+
<script src="<?php fvplus_asset('/plugins/folderview.plus/scripts/docker.runtime.diagnostics.js')?>"></script>
6769
<script defer src="<?php fvplus_asset('/plugins/folderview.plus/scripts/docker.js')?>"></script>
6870

6971
<link rel="stylesheet" href="<?php fvplus_asset('/plugins/folderview.plus/styles/runtime.shared.css')?>">

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

Lines changed: 150 additions & 627 deletions
Large diffs are not rendered by default.

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

Lines changed: 587 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
// @ts-check
2+
(function dockerRuntimeHostGuardsModule(root, factory) {
3+
const fallbackWindow = typeof globalThis !== 'undefined'
4+
? globalThis
5+
: (typeof window !== 'undefined' ? window : {});
6+
if (typeof module === 'object' && module.exports) {
7+
module.exports = factory(fallbackWindow);
8+
return;
9+
}
10+
root.FolderViewPlusDockerHostGuards = factory(fallbackWindow);
11+
root.FolderViewPlusDockerHostGuardsModuleLoaded = true;
12+
}(typeof window !== 'undefined' ? window : {}, function dockerRuntimeHostGuardsFactory(fallbackWindow) {
13+
'use strict';
14+
15+
const DEFAULT_REQUIRED_SELECTORS = Object.freeze([
16+
{ label: 'Docker table shell', selector: 'table#docker_containers' },
17+
{ label: 'Docker table body', selector: 'tbody#docker_list' },
18+
{ label: 'Docker header row', selector: '#docker_containers > thead > tr' }
19+
]);
20+
21+
const createApi = (deps = {}) => {
22+
const win = deps.window || fallbackWindow;
23+
const doc = deps.document || win.document || null;
24+
const setModuleStatus = typeof deps.setModuleStatus === 'function' ? deps.setModuleStatus : () => {};
25+
const markStep = typeof deps.markStep === 'function' ? deps.markStep : () => {};
26+
const reportFatalRuntimeError = typeof deps.reportFatalRuntimeError === 'function'
27+
? deps.reportFatalRuntimeError
28+
: () => {};
29+
const reportDegradedRuntimeState = typeof deps.reportDegradedRuntimeState === 'function'
30+
? deps.reportDegradedRuntimeState
31+
: () => {};
32+
const requiredSelectors = Array.isArray(deps.requiredSelectors) && deps.requiredSelectors.length > 0
33+
? deps.requiredSelectors
34+
: DEFAULT_REQUIRED_SELECTORS;
35+
36+
/** @type {Record<string, { available: boolean, wrapped: boolean, callCount: number, notes: string[], lastSeenAt: string | null, lastInvokedAt: string | null }>} */
37+
const hookStates = Object.create(null);
38+
39+
const ensureHookRecord = (name) => {
40+
const safeName = String(name || '').trim() || 'unknown';
41+
if (!hookStates[safeName]) {
42+
hookStates[safeName] = {
43+
available: false,
44+
wrapped: false,
45+
callCount: 0,
46+
notes: [],
47+
lastSeenAt: null,
48+
lastInvokedAt: null
49+
};
50+
}
51+
return hookStates[safeName];
52+
};
53+
54+
const collectHostPageStructureIssues = () => {
55+
const missing = [];
56+
requiredSelectors.forEach((entry) => {
57+
if (!entry || !entry.selector) {
58+
return;
59+
}
60+
if (!doc || typeof doc.querySelector !== 'function' || !doc.querySelector(entry.selector)) {
61+
missing.push(`${entry.label}: ${entry.selector}`);
62+
}
63+
});
64+
return missing;
65+
};
66+
67+
const ensureHostPageStructure = () => {
68+
const missing = collectHostPageStructureIssues();
69+
if (missing.length <= 0) {
70+
setModuleStatus('host-page-structure', 'ok', 'expected Docker host selectors detected');
71+
return;
72+
}
73+
setModuleStatus('host-page-structure', 'missing', missing.join(' | '));
74+
const error = new Error(`Expected Docker host page selectors were not found: ${missing.join(', ')}`);
75+
error.fvplusPhase = 'host-dom';
76+
error.fvplusCategory = 'host-page-structure';
77+
reportFatalRuntimeError(error, {
78+
title: 'Docker page structure changed',
79+
message: 'FolderView Plus expected the standard Unraid Docker table markup, but required host page elements were missing.',
80+
code: 'FVPLUS-DKR-DOM-001',
81+
phase: 'host-dom',
82+
category: 'host-page-structure',
83+
detailLabel: 'Missing selectors',
84+
details: missing
85+
});
86+
throw error;
87+
};
88+
89+
const captureHostHook = (name, value, options = {}) => {
90+
const safeName = String(name || '').trim() || 'unknown';
91+
const record = ensureHookRecord(safeName);
92+
const available = typeof value === 'function';
93+
record.available = available;
94+
record.lastSeenAt = new Date().toISOString();
95+
if (available) {
96+
record.notes.push(String(options.note || 'captured').trim() || 'captured');
97+
markStep(String(options.step || `${safeName} hook captured`).trim() || `${safeName} hook captured`);
98+
}
99+
if (record.notes.length > 12) {
100+
record.notes = record.notes.slice(-12);
101+
}
102+
return available;
103+
};
104+
105+
const reportMissingHook = (name, message, options = {}) => {
106+
const safeName = String(name || '').trim() || 'unknown';
107+
const record = ensureHookRecord(safeName);
108+
record.available = false;
109+
record.notes.push(String(message || 'missing').trim() || 'missing');
110+
if (record.notes.length > 12) {
111+
record.notes = record.notes.slice(-12);
112+
}
113+
reportDegradedRuntimeState(String(message || `${safeName} hook unavailable`), {
114+
phase: String(options.phase || 'hook-install').trim() || 'hook-install',
115+
category: String(options.category || 'host-hook-missing').trim() || 'host-hook-missing',
116+
detailLabel: String(options.detailLabel || 'Missing host hooks').trim() || 'Missing host hooks',
117+
details: Array.isArray(options.details) && options.details.length > 0
118+
? options.details
119+
: [`${safeName} was not a function when FolderView Plus initialized.`]
120+
});
121+
};
122+
123+
const noteHookWrapped = (name, options = {}) => {
124+
const safeName = String(name || '').trim() || 'unknown';
125+
const record = ensureHookRecord(safeName);
126+
record.wrapped = true;
127+
record.notes.push(String(options.note || 'wrapped').trim() || 'wrapped');
128+
if (record.notes.length > 12) {
129+
record.notes = record.notes.slice(-12);
130+
}
131+
markStep(String(options.step || `${safeName} hook wrapped`).trim() || `${safeName} hook wrapped`);
132+
};
133+
134+
const noteHookInvocation = (name, options = {}) => {
135+
const safeName = String(name || '').trim() || 'unknown';
136+
const record = ensureHookRecord(safeName);
137+
record.callCount += 1;
138+
record.lastInvokedAt = new Date().toISOString();
139+
const note = String(options.note || '').trim();
140+
if (note) {
141+
record.notes.push(note);
142+
if (record.notes.length > 12) {
143+
record.notes = record.notes.slice(-12);
144+
}
145+
}
146+
};
147+
148+
const getHookStates = () => JSON.parse(JSON.stringify(hookStates));
149+
150+
return {
151+
ensureHostPageStructure,
152+
collectHostPageStructureIssues,
153+
captureHostHook,
154+
reportMissingHook,
155+
noteHookWrapped,
156+
noteHookInvocation,
157+
getHookStates
158+
};
159+
};
160+
161+
return {
162+
DEFAULT_REQUIRED_SELECTORS,
163+
createApi
164+
};
165+
}));

tests/docker-runtime-shared-architecture.test.mjs

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ const dockerRuntimeInfoJs = read('src/folderview.plus/usr/local/emhttp/plugins/f
1414
const dockerPreviewActionsJs = read('src/folderview.plus/usr/local/emhttp/plugins/folderview.plus/scripts/docker.runtime.preview-actions.js');
1515
const dockerRuntimeHierarchyJs = read('src/folderview.plus/usr/local/emhttp/plugins/folderview.plus/scripts/docker.runtime.hierarchy.js');
1616
const dockerRuntimeActionsJs = read('src/folderview.plus/usr/local/emhttp/plugins/folderview.plus/scripts/docker.runtime.actions.js');
17+
const dockerRuntimeHostGuardsJs = read('src/folderview.plus/usr/local/emhttp/plugins/folderview.plus/scripts/docker.runtime.host-guards.js');
18+
const dockerRuntimeDiagnosticsJs = read('src/folderview.plus/usr/local/emhttp/plugins/folderview.plus/scripts/docker.runtime.diagnostics.js');
1719
const dockerJs = read('src/folderview.plus/usr/local/emhttp/plugins/folderview.plus/scripts/docker.js');
1820
const dockerCss = read('src/folderview.plus/usr/local/emhttp/plugins/folderview.plus/styles/docker.css');
1921
const runtimeSharedCss = read('src/folderview.plus/usr/local/emhttp/plugins/folderview.plus/styles/runtime.shared.css');
@@ -28,6 +30,8 @@ test('docker runtime page loads shared runtime module before docker modules/runt
2830
const previewActionsIndex = dockerPage.indexOf('/plugins/folderview.plus/scripts/docker.runtime.preview-actions.js');
2931
const runtimeHierarchyIndex = dockerPage.indexOf('/plugins/folderview.plus/scripts/docker.runtime.hierarchy.js');
3032
const runtimeActionsIndex = dockerPage.indexOf('/plugins/folderview.plus/scripts/docker.runtime.actions.js');
33+
const hostGuardsIndex = dockerPage.indexOf('/plugins/folderview.plus/scripts/docker.runtime.host-guards.js');
34+
const diagnosticsIndex = dockerPage.indexOf('/plugins/folderview.plus/scripts/docker.runtime.diagnostics.js');
3135
const runtimeIndex = dockerPage.indexOf('/plugins/folderview.plus/scripts/docker.js');
3236
const sharedCssIndex = dockerPage.indexOf('/plugins/folderview.plus/styles/runtime.shared.css');
3337
const dockerCssIndex = dockerPage.indexOf('/plugins/folderview.plus/styles/docker.css');
@@ -40,6 +44,8 @@ test('docker runtime page loads shared runtime module before docker modules/runt
4044
assert.ok(previewActionsIndex >= 0, 'docker preview actions script include is missing');
4145
assert.ok(runtimeHierarchyIndex >= 0, 'docker hierarchy script include is missing');
4246
assert.ok(runtimeActionsIndex >= 0, 'docker actions script include is missing');
47+
assert.ok(hostGuardsIndex >= 0, 'docker host guards script include is missing');
48+
assert.ok(diagnosticsIndex >= 0, 'docker diagnostics script include is missing');
4349
assert.ok(runtimeIndex >= 0, 'docker runtime script include is missing');
4450
assert.ok(sharedCssIndex >= 0, 'shared runtime stylesheet include is missing');
4551
assert.ok(dockerCssIndex >= 0, 'docker stylesheet include is missing');
@@ -56,7 +62,9 @@ test('docker runtime page loads shared runtime module before docker modules/runt
5662
assert.ok(runtimeInfoIndex < previewActionsIndex, 'docker.runtime.info.js must load before docker.runtime.preview-actions.js');
5763
assert.ok(previewActionsIndex < runtimeHierarchyIndex, 'docker preview action helpers must load before docker.runtime.hierarchy.js');
5864
assert.ok(runtimeHierarchyIndex < runtimeActionsIndex, 'docker hierarchy helpers must load before docker.runtime.actions.js');
59-
assert.ok(runtimeActionsIndex < runtimeIndex, 'docker action helpers must load before docker.js');
65+
assert.ok(runtimeActionsIndex < hostGuardsIndex, 'docker action helpers must load before docker.runtime.host-guards.js');
66+
assert.ok(hostGuardsIndex < diagnosticsIndex, 'docker host guards must load before docker.runtime.diagnostics.js');
67+
assert.ok(diagnosticsIndex < runtimeIndex, 'docker diagnostics helpers must load before docker.js');
6068
assert.ok(stateObserverIndex < runtimeIndex, 'runtime state observer module must load before docker.js');
6169
assert.ok(sharedIndex < runtimeIndex, 'shared runtime must load before docker.js');
6270
assert.ok(sharedCssIndex < dockerCssIndex, 'shared runtime stylesheet must load before docker.css');
@@ -83,6 +91,16 @@ test('docker extracted helper modules export createApi entry points with safe gl
8391
assert.match(dockerRuntimeActionsJs, /root\.FolderViewPlusDockerRuntimeActions = factory\(\);/);
8492
assert.match(dockerRuntimeActionsJs, /root\.FolderViewPlusDockerRuntimeActionsModuleLoaded = true;/);
8593
assert.match(dockerRuntimeActionsJs, /const createApi = \(deps = \{\}\) =>/);
94+
assert.match(dockerRuntimeHostGuardsJs, /^\/\/ @ts-check/m);
95+
assert.match(dockerRuntimeHostGuardsJs, /const fallbackWindow = typeof globalThis !== 'undefined'/);
96+
assert.match(dockerRuntimeHostGuardsJs, /root\.FolderViewPlusDockerHostGuards = factory\(fallbackWindow\);/);
97+
assert.match(dockerRuntimeHostGuardsJs, /root\.FolderViewPlusDockerHostGuardsModuleLoaded = true;/);
98+
assert.match(dockerRuntimeHostGuardsJs, /const createApi = \(deps = \{\}\) =>/);
99+
assert.match(dockerRuntimeDiagnosticsJs, /^\/\/ @ts-check/m);
100+
assert.match(dockerRuntimeDiagnosticsJs, /const fallbackWindow = typeof globalThis !== 'undefined'/);
101+
assert.match(dockerRuntimeDiagnosticsJs, /root\.FolderViewPlusDockerRuntimeDiagnostics = factory\(fallbackWindow\);/);
102+
assert.match(dockerRuntimeDiagnosticsJs, /root\.FolderViewPlusDockerRuntimeDiagnosticsModuleLoaded = true;/);
103+
assert.match(dockerRuntimeDiagnosticsJs, /const createApi = \(deps = \{\}\) =>/);
86104
});
87105

88106
test('docker shared runtime module binds to the shared folder contract and exports runtime primitives', () => {
@@ -114,6 +132,8 @@ test('docker runtime consumes shared state store and guarded async action wrappe
114132
assert.match(dockerJs, /const dockerPreviewActionsModule = window\.FolderViewPlusDockerPreviewActions \|\| null;/);
115133
assert.match(dockerJs, /const dockerRuntimeHierarchyModule = window\.FolderViewPlusDockerRuntimeHierarchy \|\| null;/);
116134
assert.match(dockerJs, /const dockerRuntimeActionsModule = window\.FolderViewPlusDockerRuntimeActions \|\| null;/);
135+
assert.match(dockerJs, /const dockerHostGuardsModule = window\.FolderViewPlusDockerHostGuards \|\| null;/);
136+
assert.match(dockerJs, /const dockerRuntimeDiagnosticsModule = window\.FolderViewPlusDockerRuntimeDiagnostics \|\| null;/);
117137
assert.match(dockerJs, /const fatalBanner = window\.FolderViewPlusFatalBanner \|\| null;/);
118138
assert.match(dockerJs, /const DOCKER_FATAL_BANNER_HOST_SELECTOR = String\(dockerFatalBannerRuntimeConfig\.hostSelector \|\| '#fvplus-docker-runtime-banner-host, \.canvas'\)/);
119139
assert.match(dockerJs, /const createDockerRuntimeDiagnosticsBridge = typeof dockerRuntimeShared\.createRuntimeDiagnosticsBridge === 'function'/);
@@ -124,6 +144,13 @@ test('docker runtime consumes shared state store and guarded async action wrappe
124144
assert.match(dockerJs, /dockerBootstrapMissingModules\.push\('docker\.runtime\.preview-actions\.js'\)/);
125145
assert.match(dockerJs, /dockerBootstrapMissingModules\.push\('docker\.runtime\.hierarchy\.js'\)/);
126146
assert.match(dockerJs, /dockerBootstrapMissingModules\.push\('docker\.runtime\.actions\.js'\)/);
147+
assert.match(dockerJs, /dockerBootstrapMissingModules\.push\('docker\.runtime\.host-guards\.js'\)/);
148+
assert.match(dockerJs, /dockerBootstrapMissingModules\.push\('docker\.runtime\.diagnostics\.js'\)/);
149+
assert.match(dockerJs, /const getDockerHostGuardsApi = \(\) => \{/);
150+
assert.match(dockerJs, /dockerHostGuardsModule\.createApi\(\{/);
151+
assert.match(dockerJs, /const getDockerRuntimeDiagnosticsApi = \(\) => \{/);
152+
assert.match(dockerJs, /dockerRuntimeDiagnosticsModule\.createApi\(\{/);
153+
assert.match(dockerJs, /const buildDockerDiagnosticsCorrelationContext = \(\) => \(\{/);
127154
assert.match(dockerJs, /const getDockerRuntimeInfoApi = \(\) => \{/);
128155
assert.match(dockerJs, /dockerRuntimeInfoModule\.createApi\(\{/);
129156
assert.match(dockerJs, /const getDockerPreviewActionsApi = \(\) => \{/);

0 commit comments

Comments
 (0)