Skip to content

Commit 51318bf

Browse files
feat: ship advanced templates, diagnostics, and runtime controls
1 parent 40c58b1 commit 51318bf

14 files changed

Lines changed: 1843 additions & 72 deletions

File tree

CHANGELOG-fixes.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
# FolderView Plus Changelog
22

3+
## Version 2026.03.06.7
4+
5+
- Add folder templates support (save/apply/delete) with server endpoint and settings UI.
6+
- Add conflict inspector report for Docker/VM assignment overlap and exclude-rule blocking visibility.
7+
- Add support bundle export flow with Full/Sanitized mode selection.
8+
- Add runtime live auto-refresh support (enable/disable + interval) for Docker/VM/Dashboard.
9+
- Add runtime performance mode behavior that disables heavy preview features at render time.
10+
- Improve rule engine consistency so matching exclude rules always block final assignment.
11+
312
## Version 2026.03.05.5
413

514
- Move VMs directly below Docker in settings for a cleaner reading order.
199 KB
Binary file not shown.

folderview.plus.plg

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,26 @@
66
<!ENTITY launch "Settings/FolderViewPlus">
77
<!ENTITY plugdir "/usr/local/emhttp/plugins/&name;">
88
<!ENTITY pluginURL "https://raw.githubusercontent.com/&github;/main/folderview.plus.plg">
9-
<!ENTITY version "2026.03.06.6">
10-
<!ENTITY md5 "94dfbc5312d0459896a9ef0b8787545d">
9+
<!ENTITY version "2026.03.06.7">
10+
<!ENTITY md5 "0592669b091cb38c3f2e394e138c9c2b">
1111
]>
1212

1313
<PLUGIN name="&name;" author="&author;" version="&version;" launch="&launch;" pluginURL="&pluginURL;" icon="folder-open-o" support="https://github.com/alexphillips-dev/FolderView-Plus/issues" min="7.0.0">
1414
<CHANGES>
1515

16+
###2026.03.06.7
17+
- Complete the advanced settings update set:
18+
- add folder templates API/UI (save/apply/delete),
19+
- add conflict inspector report UI,
20+
- add support-bundle export flow with Full/Sanitized mode selection.
21+
- Expand runtime preferences support:
22+
- add live auto-refresh timers honoring per-type interval and enable toggles,
23+
- add performance mode behavior on Docker/VM/Dashboard by disabling heavy folder preview features at runtime.
24+
- Expand diagnostics and compatibility foundations:
25+
- add path repair action endpoint integration,
26+
- add advanced rule kinds/effects plus exclude precedence consistency in JS/PHP decision logic,
27+
- add richer prefs schema normalization (runtime + backup schedule fields) and test coverage.
28+
1629
###2026.03.06.6
1730
- Add compatibility migration for legacy Folder View installs:
1831
- auto-import folder data from `/boot/config/plugins/folder.view3` and `/boot/config/plugins/folder.view2` when `folderview.plus` data is empty/missing,

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

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,12 @@ require_once('/usr/local/emhttp/plugins/folderview.plus/langs/script.php');
4646
<label><input id="docker-badge-stopped" type="checkbox" onchange="changeBadgePref('docker', 'stopped', this.checked)"> Stopped</label>
4747
<label><input id="docker-badge-updates" type="checkbox" onchange="changeBadgePref('docker', 'updates', this.checked)"> Updates</label>
4848
</div>
49+
<div class="badge-toggle-row">
50+
<span class="badge-toggle-title">Runtime</span>
51+
<label><input id="docker-live-refresh-enabled" type="checkbox" onchange="changeRuntimePref('docker', 'liveRefreshEnabled', this.checked)"> Live auto-refresh</label>
52+
<label>Interval (sec): <input id="docker-live-refresh-seconds" type="number" min="10" max="300" step="1" onchange="changeRuntimePref('docker', 'liveRefreshSeconds', this.value)"></label>
53+
<label><input id="docker-performance-mode" type="checkbox" onchange="changeRuntimePref('docker', 'performanceMode', this.checked)"> Performance mode</label>
54+
</div>
4955
</div>
5056
</div>
5157
<div class="table-wrap">
@@ -94,6 +100,12 @@ require_once('/usr/local/emhttp/plugins/folderview.plus/langs/script.php');
94100
<label><input id="vm-badge-running" type="checkbox" onchange="changeBadgePref('vm', 'running', this.checked)"> Running</label>
95101
<label><input id="vm-badge-stopped" type="checkbox" onchange="changeBadgePref('vm', 'stopped', this.checked)"> Stopped</label>
96102
</div>
103+
<div class="badge-toggle-row">
104+
<span class="badge-toggle-title">Runtime</span>
105+
<label><input id="vm-live-refresh-enabled" type="checkbox" onchange="changeRuntimePref('vm', 'liveRefreshEnabled', this.checked)"> Live auto-refresh</label>
106+
<label>Interval (sec): <input id="vm-live-refresh-seconds" type="number" min="10" max="300" step="1" onchange="changeRuntimePref('vm', 'liveRefreshSeconds', this.value)"></label>
107+
<label><input id="vm-performance-mode" type="checkbox" onchange="changeRuntimePref('vm', 'performanceMode', this.checked)"> Performance mode</label>
108+
</div>
97109
</div>
98110
</div>
99111
<div class="table-wrap">
@@ -129,9 +141,17 @@ require_once('/usr/local/emhttp/plugins/folderview.plus/langs/script.php');
129141
</div>
130142
<div class="rules-editor">
131143
<select id="docker-rule-folder"></select>
144+
<select id="docker-rule-effect">
145+
<option value="include">Include</option>
146+
<option value="exclude">Exclude</option>
147+
</select>
132148
<select id="docker-rule-kind" onchange="toggleRuleKindFields('docker')">
133149
<option value="name_regex">Name regex</option>
134-
<option value="label">Docker label</option>
150+
<option value="label">Label equals</option>
151+
<option value="label_contains">Label contains</option>
152+
<option value="label_starts_with">Label starts with</option>
153+
<option value="image_regex">Image regex</option>
154+
<option value="compose_project_regex">Compose project regex</option>
135155
</select>
136156
<input id="docker-rule-pattern" type="text" placeholder="Regex pattern (example: ^media-)">
137157
<input id="docker-rule-label-key" type="text" placeholder="Label key (example: com.example.stack)" style="display:none;">
@@ -142,6 +162,8 @@ require_once('/usr/local/emhttp/plugins/folderview.plus/langs/script.php');
142162
<input id="docker-rule-test-name" type="text" placeholder="Test name (example: media-server)">
143163
<input id="docker-rule-test-label-key" type="text" placeholder="Test label key (optional)">
144164
<input id="docker-rule-test-label-value" type="text" placeholder="Test label value (optional)">
165+
<input id="docker-rule-test-image" type="text" placeholder="Test image (optional, example: linuxserver/sonarr)">
166+
<input id="docker-rule-test-compose" type="text" placeholder="Test compose project (optional)">
145167
<button onclick="testAutoRule('docker')"><i class="fa fa-flask"></i> Test rule priority</button>
146168
<div id="docker-rule-test-output" class="rule-test-output"></div>
147169
</div>
@@ -165,6 +187,10 @@ require_once('/usr/local/emhttp/plugins/folderview.plus/langs/script.php');
165187
</div>
166188
<div class="rules-editor">
167189
<select id="vm-rule-folder"></select>
190+
<select id="vm-rule-effect">
191+
<option value="include">Include</option>
192+
<option value="exclude">Exclude</option>
193+
</select>
168194
<select id="vm-rule-kind">
169195
<option value="name_regex">Name regex</option>
170196
</select>
@@ -231,6 +257,12 @@ require_once('/usr/local/emhttp/plugins/folderview.plus/langs/script.php');
231257
<button onclick="restoreLatestBackup('docker')"><i class="fa fa-history"></i> Restore latest</button>
232258
<button onclick="refreshBackups('docker')"><i class="fa fa-refresh"></i> Refresh list</button>
233259
</div>
260+
<div class="schedule-row">
261+
<label><input id="docker-backup-schedule-enabled" type="checkbox" onchange="changeBackupSchedulePref('docker', 'enabled', this.checked)"> Scheduled backups</label>
262+
<label>Interval (hours): <input id="docker-backup-interval-hours" type="number" min="1" max="168" step="1" onchange="changeBackupSchedulePref('docker', 'intervalHours', this.value)"></label>
263+
<label>Retention: <input id="docker-backup-retention" type="number" min="1" max="200" step="1" onchange="changeBackupSchedulePref('docker', 'retention', this.value)"></label>
264+
<span id="docker-backup-last-run" class="schedule-hint"></span>
265+
</div>
234266
<table class="rules-table">
235267
<thead>
236268
<tr>
@@ -254,6 +286,12 @@ require_once('/usr/local/emhttp/plugins/folderview.plus/langs/script.php');
254286
<button onclick="restoreLatestBackup('vm')"><i class="fa fa-history"></i> Restore latest</button>
255287
<button onclick="refreshBackups('vm')"><i class="fa fa-refresh"></i> Refresh list</button>
256288
</div>
289+
<div class="schedule-row">
290+
<label><input id="vm-backup-schedule-enabled" type="checkbox" onchange="changeBackupSchedulePref('vm', 'enabled', this.checked)"> Scheduled backups</label>
291+
<label>Interval (hours): <input id="vm-backup-interval-hours" type="number" min="1" max="168" step="1" onchange="changeBackupSchedulePref('vm', 'intervalHours', this.value)"></label>
292+
<label>Retention: <input id="vm-backup-retention" type="number" min="1" max="200" step="1" onchange="changeBackupSchedulePref('vm', 'retention', this.value)"></label>
293+
<span id="vm-backup-last-run" class="schedule-hint"></span>
294+
</div>
257295
<table class="rules-table">
258296
<thead>
259297
<tr>
@@ -269,6 +307,54 @@ require_once('/usr/local/emhttp/plugins/folderview.plus/langs/script.php');
269307
</div>
270308
<hr>
271309

310+
<h2>Folder templates</h2>
311+
<div class="template-grid">
312+
<div class="rules-panel">
313+
<div class="rules-header">
314+
<h3>Docker templates</h3>
315+
<span class="rules-help">Save folder look/behavior presets and apply them to any folder.</span>
316+
</div>
317+
<div class="template-controls">
318+
<select id="docker-template-source-folder"></select>
319+
<input id="docker-template-name" type="text" placeholder="Template name">
320+
<button onclick="createTemplateFromFolder('docker')"><i class="fa fa-save"></i> Save template from folder</button>
321+
</div>
322+
<table class="rules-table">
323+
<thead>
324+
<tr>
325+
<th>Name</th>
326+
<th>Updated</th>
327+
<th>Action</th>
328+
</tr>
329+
</thead>
330+
<tbody id="docker-templates"></tbody>
331+
</table>
332+
</div>
333+
334+
<div class="rules-panel">
335+
<div class="rules-header">
336+
<h3>VM templates</h3>
337+
<span class="rules-help">Save VM folder presets and apply them without manual reconfiguration.</span>
338+
</div>
339+
<div class="template-controls">
340+
<select id="vm-template-source-folder"></select>
341+
<input id="vm-template-name" type="text" placeholder="Template name">
342+
<button onclick="createTemplateFromFolder('vm')"><i class="fa fa-save"></i> Save template from folder</button>
343+
</div>
344+
<table class="rules-table">
345+
<thead>
346+
<tr>
347+
<th>Name</th>
348+
<th>Updated</th>
349+
<th>Action</th>
350+
</tr>
351+
</thead>
352+
<tbody id="vm-templates"></tbody>
353+
</table>
354+
</div>
355+
</div>
356+
<hr>
357+
272358
<h2>Diagnostics</h2>
273359
<div class="rules-panel">
274360
<div class="rules-header">
@@ -279,14 +365,31 @@ require_once('/usr/local/emhttp/plugins/folderview.plus/langs/script.php');
279365
<button onclick="runDiagnostics()"><i class="fa fa-stethoscope"></i> Run health check</button>
280366
<button onclick="repairDiagnostics('sync_docker_order')"><i class="fa fa-sort"></i> Rebuild Docker order index</button>
281367
<button onclick="repairDiagnostics('normalize_prefs')"><i class="fa fa-wrench"></i> Validate and normalize prefs</button>
368+
<button onclick="repairDiagnostics('repair_paths')"><i class="fa fa-folder-open"></i> Repair plugin paths</button>
282369
<button onclick="exportDiagnostics()"><i class="fa fa-download"></i> Export diagnostics JSON</button>
370+
<button onclick="exportSupportBundle()"><i class="fa fa-life-ring"></i> Export support bundle</button>
283371
</div>
284372
<pre id="diagnostics-output" class="diagnostics-output">Run health check to view diagnostics.</pre>
285373
</div>
374+
<hr>
375+
376+
<h2>Conflict inspector</h2>
377+
<div class="rules-panel">
378+
<div class="rules-header">
379+
<h3>Folder membership conflict report</h3>
380+
<span class="rules-help">Shows why each item is assigned (manual/regex/label/rule), including overlaps and blocked rules.</span>
381+
</div>
382+
<div class="backup-actions">
383+
<button onclick="runConflictInspector('docker')"><i class="fa fa-search"></i> Scan Docker conflicts</button>
384+
<button onclick="runConflictInspector('vm')"><i class="fa fa-search"></i> Scan VM conflicts</button>
385+
</div>
386+
<pre id="conflict-output" class="diagnostics-output">Run a conflict scan to view results.</pre>
387+
</div>
286388

287389
<div id="import-preview-dialog" style="display:none;">
288390
<p><strong>Import summary</strong></p>
289391
<div id="import-preview-meta" class="preview-meta"></div>
392+
<div id="import-preview-result" class="preview-result"></div>
290393
<label for="import-mode-select">Import mode</label>
291394
<select id="import-mode-select">
292395
<option value="merge">Merge</option>

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

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ const createFolders = async () => {
6464
prefsResponse = {};
6565
}
6666
folderTypePrefs.docker = utils.normalizePrefs(prefsResponse?.prefs || {});
67+
applyDashboardRuntimePrefs();
6768

6869
// Filter the order to get the container that aren't in the order, this happen when a new container is created
6970
let newOnes = order.filter(x => !unraidOrder.includes(x));
@@ -182,6 +183,7 @@ const createFolders = async () => {
182183
prefsResponse = {};
183184
}
184185
folderTypePrefs.vm = utils.normalizePrefs(prefsResponse?.prefs || {});
186+
applyDashboardRuntimePrefs();
185187

186188
// Filter the webui order to get the container that aren't in the order, this happen when a new container is created
187189
let newOnes = order.filter(x => !unraidOrder.includes(x));
@@ -276,6 +278,7 @@ const createFolders = async () => {
276278
}
277279

278280
folderDebugMode = false;
281+
applyDashboardRuntimePrefs();
279282
};
280283

281284
/**
@@ -289,6 +292,19 @@ const createFolders = async () => {
289292
* @returns the number of element removed before the folder
290293
*/
291294
const createFolderDocker = (folder, id, position, order, containersInfo, foldersDone) => {
295+
if (folderTypePrefs?.docker?.performanceMode === true && folder && typeof folder === 'object') {
296+
folder.settings = {
297+
...(folder.settings || {}),
298+
preview: 0,
299+
preview_hover: false,
300+
preview_logs: false,
301+
preview_console: false,
302+
preview_webui: false,
303+
preview_vertical_bars: false,
304+
preview_update: false,
305+
preview_grayscale: false
306+
};
307+
}
292308

293309
folderEvents.dispatchEvent(new CustomEvent('docker-pre-folder-creation', {detail: {
294310
folder: folder,
@@ -512,6 +528,19 @@ const createFolderDocker = (folder, id, position, order, containersInfo, folders
512528
* @returns the number of element removed before the folder
513529
*/
514530
const createFolderVM = (folder, id, position, order, vmInfo, foldersDone) => {
531+
if (folderTypePrefs?.vm?.performanceMode === true && folder && typeof folder === 'object') {
532+
folder.settings = {
533+
...(folder.settings || {}),
534+
preview: 0,
535+
preview_hover: false,
536+
preview_logs: false,
537+
preview_console: false,
538+
preview_webui: false,
539+
preview_vertical_bars: false,
540+
preview_update: false,
541+
preview_grayscale: false
542+
};
543+
}
515544

516545
folderEvents.dispatchEvent(new CustomEvent('vm-pre-folder-creation', {detail: {
517546
folder: folder,
@@ -1407,6 +1436,57 @@ let folderReq = {
14071436
docker: [],
14081437
vm: []
14091438
};
1439+
let liveRefreshTimer = null;
1440+
let liveRefreshMs = 0;
1441+
let liveRefreshInFlight = false;
1442+
1443+
const clearLiveRefreshTimer = () => {
1444+
if (liveRefreshTimer) {
1445+
clearInterval(liveRefreshTimer);
1446+
liveRefreshTimer = null;
1447+
}
1448+
liveRefreshMs = 0;
1449+
};
1450+
1451+
const runLiveRefreshTick = () => {
1452+
if (liveRefreshInFlight || document.hidden) {
1453+
return;
1454+
}
1455+
liveRefreshInFlight = true;
1456+
try {
1457+
loadlist();
1458+
} finally {
1459+
setTimeout(() => {
1460+
liveRefreshInFlight = false;
1461+
}, 1000);
1462+
}
1463+
};
1464+
1465+
const applyDashboardRuntimePrefs = () => {
1466+
const dockerPrefs = utils.normalizePrefs(folderTypePrefs?.docker || {});
1467+
const vmPrefs = utils.normalizePrefs(folderTypePrefs?.vm || {});
1468+
const candidates = [];
1469+
if ($('tbody#docker_view').length > 0 && dockerPrefs.liveRefreshEnabled === true) {
1470+
candidates.push(Math.max(10, Math.min(300, Number(dockerPrefs.liveRefreshSeconds) || 20)));
1471+
}
1472+
if ($('tbody#vm_view').length > 0 && vmPrefs.liveRefreshEnabled === true) {
1473+
candidates.push(Math.max(10, Math.min(300, Number(vmPrefs.liveRefreshSeconds) || 20)));
1474+
}
1475+
const performanceMode = dockerPrefs.performanceMode === true || vmPrefs.performanceMode === true;
1476+
$('body').toggleClass('fvplus-performance-mode', performanceMode);
1477+
1478+
if (!candidates.length) {
1479+
clearLiveRefreshTimer();
1480+
return;
1481+
}
1482+
const intervalMs = Math.min(...candidates) * 1000;
1483+
if (liveRefreshTimer && liveRefreshMs === intervalMs) {
1484+
return;
1485+
}
1486+
clearLiveRefreshTimer();
1487+
liveRefreshMs = intervalMs;
1488+
liveRefreshTimer = setInterval(runLiveRefreshTick, intervalMs);
1489+
};
14101490

14111491
// Patching the original function to make sure the containers are rendered before insering the folder
14121492
window.loadlist_original = loadlist;

0 commit comments

Comments
 (0)