Skip to content

Commit 460492d

Browse files
feat: diagnostics v2 with integrity, state snapshots, history, and privacy modes
1 parent fecfa76 commit 460492d

6 files changed

Lines changed: 822 additions & 20 deletions

File tree

191 KB
Binary file not shown.

folderview.plus.plg

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,24 @@
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.2">
10-
<!ENTITY md5 "e46f08d7df86526c083f2b4a5fe97452">
9+
<!ENTITY version "2026.03.06.3">
10+
<!ENTITY md5 "39ac8cf761703bbef75674f8105c8814">
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.3
17+
- Expand diagnostics export to schema v2 with richer bug-fixing data:
18+
- `integrityChecks` per type (duplicate names/assignments, orphaned members, invalid regex/rules, manual-order gaps),
19+
- `stateSnapshot` per folder (computed started/paused/stopped totals, effective status text, badge visibility, status colors),
20+
- `environment` block (Unraid/PHP/server/request context),
21+
- key file SHA-256 hashes for folder/prefs JSON files.
22+
- Add diagnostics history tracking (`importExportHistory`) with persistent event log for import/export/clear/reorder/backup actions.
23+
- Add diagnostics privacy modes:
24+
- default `sanitized` export (redacted paths and request-identifying details),
25+
- optional `full` export from the UI for deep support cases.
26+
1627
###2026.03.06.2
1728
- Fix folder editor alignment for the status color controls so they line up with the rest of the settings form.
1829

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

Lines changed: 91 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -106,26 +106,48 @@ const bulkAssign = async (type, folderId, items) => {
106106
return response.result;
107107
};
108108

109-
const getDiagnostics = async () => {
110-
const response = await apiGetJson('/plugins/folderview.plus/server/diagnostics.php?action=report');
109+
const getDiagnostics = async (privacy = 'sanitized') => {
110+
const response = await apiGetJson(`/plugins/folderview.plus/server/diagnostics.php?action=report&privacy=${encodeURIComponent(privacy || 'sanitized')}`);
111111
if (!response.ok) {
112112
throw new Error(response.error || 'Diagnostics failed.');
113113
}
114114
return response.diagnostics || {};
115115
};
116116

117-
const runDiagnosticAction = async (action, type) => {
117+
const runDiagnosticAction = async (action, type, privacy = 'sanitized') => {
118118
const payload = { action };
119119
if (type) {
120120
payload.type = type;
121121
}
122+
payload.privacy = privacy || 'sanitized';
122123
const response = parseJsonResponse(await $.post('/plugins/folderview.plus/server/diagnostics.php', payload).promise());
123124
if (!response.ok) {
124125
throw new Error(response.error || 'Diagnostics action failed.');
125126
}
126127
return response.diagnostics || {};
127128
};
128129

130+
const trackDiagnosticsEvent = async ({ eventType, type = null, status = 'ok', source = 'ui', details = {} }) => {
131+
if (!eventType) {
132+
return;
133+
}
134+
const payload = {
135+
action: 'track_event',
136+
eventType: String(eventType),
137+
status: String(status || 'ok'),
138+
source: String(source || 'ui'),
139+
details: JSON.stringify(details || {})
140+
};
141+
if (type) {
142+
payload.type = type;
143+
}
144+
try {
145+
await $.post('/plugins/folderview.plus/server/diagnostics.php', payload).promise();
146+
} catch (error) {
147+
// Event tracking is best-effort and should never block UI actions.
148+
}
149+
};
150+
129151
const fetchPrefs = async (type) => {
130152
try {
131153
const response = await apiGetJson(`/plugins/folderview.plus/server/prefs.php?type=${type}`);
@@ -690,6 +712,15 @@ const downloadType = (type, id) => {
690712
pluginVersion
691713
});
692714
downloadFile(`${folder.name}.json`, toPrettyJson(payload));
715+
trackDiagnosticsEvent({
716+
eventType: 'export',
717+
type,
718+
details: {
719+
mode: 'single',
720+
folderCount: 1,
721+
schemaVersion: utils.EXPORT_SCHEMA_VERSION
722+
}
723+
});
693724
return;
694725
}
695726

@@ -701,6 +732,15 @@ const downloadType = (type, id) => {
701732

702733
const name = type === 'docker' ? `${EXPORT_BASENAME}.json` : `${EXPORT_BASENAME} VM.json`;
703734
downloadFile(name, toPrettyJson(payload));
735+
trackDiagnosticsEvent({
736+
eventType: 'export',
737+
type,
738+
details: {
739+
mode: 'full',
740+
folderCount: Object.keys(folders).length,
741+
schemaVersion: utils.EXPORT_SCHEMA_VERSION
742+
}
743+
});
704744
};
705745
const importType = async (type) => {
706746
let selected;
@@ -756,6 +796,16 @@ const importType = async (type) => {
756796
const backup = await createBackup(type, `before-import-${dialogResult.mode}`);
757797
await applyImportOperations(type, operations);
758798
await Promise.all([refreshType(type), refreshBackups(type)]);
799+
await trackDiagnosticsEvent({
800+
eventType: 'import',
801+
type,
802+
details: {
803+
mode: dialogResult.mode,
804+
creates: operations.creates.length,
805+
updates: operations.upserts.length,
806+
deletes: operations.deletes.length
807+
}
808+
});
759809
await offerUndoAction(type, backup, 'Import');
760810
} catch (error) {
761811
showError('Import failed', error);
@@ -797,6 +847,14 @@ const clearType = (type, id) => {
797847
}
798848

799849
await Promise.all([refreshType(type), refreshBackups(type)]);
850+
await trackDiagnosticsEvent({
851+
eventType: id ? 'delete_folder' : 'clear_folders',
852+
type,
853+
details: {
854+
deletedCount: id ? 1 : Object.keys(folders).length,
855+
singleFolder: Boolean(id)
856+
}
857+
});
800858
await offerUndoAction(type, backup, id ? 'Delete folder' : 'Clear folders');
801859
} catch (error) {
802860
showError('Delete failed', error);
@@ -1035,6 +1093,14 @@ const assignSelectedItems = async (type) => {
10351093
const backup = await createBackup(type, 'before-bulk-assign');
10361094
await bulkAssign(type, folderId, selected);
10371095
await Promise.all([refreshType(type), refreshBackups(type)]);
1096+
await trackDiagnosticsEvent({
1097+
eventType: 'bulk_assign',
1098+
type,
1099+
details: {
1100+
folderId,
1101+
itemCount: selected.length
1102+
}
1103+
});
10381104
await offerUndoAction(type, backup, 'Bulk assignment');
10391105
} catch (error) {
10401106
showError('Bulk assignment failed', error);
@@ -1171,15 +1237,30 @@ const repairDiagnostics = async (action) => {
11711237
};
11721238

11731239
const exportDiagnostics = async () => {
1174-
try {
1175-
if (!lastDiagnostics) {
1176-
const diagnostics = await getDiagnostics();
1240+
swal({
1241+
title: 'Export diagnostics',
1242+
text: 'Include full details (paths, names, and request metadata)?\nChoose Cancel for sanitized export.',
1243+
type: 'warning',
1244+
showCancelButton: true,
1245+
confirmButtonText: 'Full export',
1246+
cancelButtonText: 'Sanitized export'
1247+
}, async (useFull) => {
1248+
const privacy = useFull ? 'full' : 'sanitized';
1249+
try {
1250+
const diagnostics = await getDiagnostics(privacy);
11771251
renderDiagnostics(diagnostics);
1252+
downloadFile('FolderView Plus Diagnostics.json', toPrettyJson(diagnostics || {}));
1253+
await trackDiagnosticsEvent({
1254+
eventType: 'diagnostics_export',
1255+
details: {
1256+
privacyMode: privacy,
1257+
schemaVersion: diagnostics?.schemaVersion || null
1258+
}
1259+
});
1260+
} catch (error) {
1261+
showError('Diagnostics export failed', error);
11781262
}
1179-
downloadFile('FolderView Plus Diagnostics.json', toPrettyJson(lastDiagnostics || {}));
1180-
} catch (error) {
1181-
showError('Diagnostics export failed', error);
1182-
}
1263+
});
11831264
};
11841265

11851266
const checkForUpdatesNow = async () => {

src/folderview.plus/usr/local/emhttp/plugins/folderview.plus/server/backup.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@
1313
if (!file_exists($path)) {
1414
throw new RuntimeException('Backup file not found.');
1515
}
16+
try {
17+
appendDiagnosticsHistoryEvent('backup_download', $type, [
18+
'name' => basename($path)
19+
], 'ok', 'server');
20+
} catch (Throwable $err) {
21+
// Non-fatal.
22+
}
1623
header_remove('Content-Type');
1724
header('Content-Type: application/json');
1825
header('Content-Disposition: attachment; filename="' . basename($path) . '"');

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

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,41 @@
55

66
try {
77
$action = (string)($_REQUEST['action'] ?? 'report');
8+
$privacyMode = normalizeDiagnosticsPrivacyMode((string)($_REQUEST['privacy'] ?? FVPLUS_DIAGNOSTICS_DEFAULT_PRIVACY));
9+
10+
if ($action === 'track_event') {
11+
$eventType = trim((string)($_REQUEST['eventType'] ?? ''));
12+
if ($eventType === '') {
13+
throw new RuntimeException('Event type is required.');
14+
}
15+
$type = null;
16+
if (isset($_REQUEST['type']) && $_REQUEST['type'] !== '') {
17+
$type = ensureType((string)$_REQUEST['type']);
18+
}
19+
$status = (string)($_REQUEST['status'] ?? 'ok');
20+
$source = (string)($_REQUEST['source'] ?? 'ui');
21+
$detailsRaw = $_REQUEST['details'] ?? null;
22+
$details = [];
23+
if (is_string($detailsRaw) && $detailsRaw !== '') {
24+
$parsed = json_decode($detailsRaw, true);
25+
if (is_array($parsed)) {
26+
$details = $parsed;
27+
}
28+
} elseif (is_array($detailsRaw)) {
29+
$details = $detailsRaw;
30+
}
31+
$event = appendDiagnosticsHistoryEvent($eventType, $type, $details, $status, $source);
32+
echo json_encode([
33+
'ok' => true,
34+
'event' => $event
35+
]);
36+
exit;
37+
}
838

939
if ($action === 'report') {
1040
echo json_encode([
1141
'ok' => true,
12-
'diagnostics' => getDiagnosticsSnapshot()
42+
'diagnostics' => getDiagnosticsSnapshot($privacyMode)
1343
]);
1444
exit;
1545
}
@@ -19,7 +49,7 @@
1949
echo json_encode([
2050
'ok' => true,
2151
'message' => 'Docker order sync completed.',
22-
'diagnostics' => getDiagnosticsSnapshot()
52+
'diagnostics' => getDiagnosticsSnapshot($privacyMode)
2353
]);
2454
exit;
2555
}
@@ -33,7 +63,7 @@
3363
echo json_encode([
3464
'ok' => true,
3565
'message' => 'Preferences normalized and rewritten.',
36-
'diagnostics' => getDiagnosticsSnapshot()
66+
'diagnostics' => getDiagnosticsSnapshot($privacyMode)
3767
]);
3868
exit;
3969
}
@@ -44,7 +74,7 @@
4474
echo json_encode([
4575
'ok' => true,
4676
'backup' => $backup,
47-
'diagnostics' => getDiagnosticsSnapshot()
77+
'diagnostics' => getDiagnosticsSnapshot($privacyMode)
4878
]);
4979
exit;
5080
}

0 commit comments

Comments
 (0)