From 233b967032c66b89c30c55258e673f6c4904a2c4 Mon Sep 17 00:00:00 2001 From: MarekWo Date: Sun, 26 Jul 2026 18:45:17 +0200 Subject: [PATCH] feat(pathanalyzer): remember filter settings between visits Time range and the filter bar reset on every open, so a working set like "Last 1 day + 2/3-byte" had to be re-entered each time. The controls are now stored in localStorage - a personal working set, not device state, so it stays out of the database. Only user-driven changes are stored: programmatic ones, such as the deep-link widening the range to 7 days to find an older message, must not overwrite the saved set. Restoring is skipped entirely when arriving via a deep link, where a saved filter could hide the very message the user clicked through to. The Routes segment length rides along - same "set it again every visit" annoyance. A stored value for an option that no longer exists is ignored. Co-Authored-By: Claude Opus 5 --- app/static/js/path-analyzer.js | 74 +++++++++++++++++++++++++++++++--- 1 file changed, 69 insertions(+), 5 deletions(-) diff --git a/app/static/js/path-analyzer.js b/app/static/js/path-analyzer.js index 2070e07..a232881 100644 --- a/app/static/js/path-analyzer.js +++ b/app/static/js/path-analyzer.js @@ -1172,7 +1172,64 @@ function paClearFilters() { document.getElementById('paTokenFilter').value = ''; document.getElementById('paSenderFilter').value = ''; document.getElementById('paContentFilter').value = ''; + paApplyAndSaveFilters(); +} + +// ================================================================ +// Filter persistence +// ================================================================ + +// Browser-local only - these are a personal working set, not device state, +// so they stay out of the database +const PA_FILTERS_KEY = 'mc-webui-pa-filters'; + +// The toolbar controls, plus the Routes segment length - same "set it +// again on every visit" annoyance +const PA_SAVED_CONTROLS = [ + 'paDaysSelect', 'paHopsFilter', 'paHashSizeFilter', + 'paTokenFilter', 'paSenderFilter', 'paContentFilter', + 'paSegLenSelect', +]; + +function paSaveFilters() { + const state = {}; + PA_SAVED_CONTROLS.forEach(id => { state[id] = document.getElementById(id).value; }); + try { + localStorage.setItem(PA_FILTERS_KEY, JSON.stringify(state)); + } catch (e) { + // Private mode or a full quota - remembering filters is a convenience, + // never a reason to break the panel + console.warn('Could not save filters:', e); + } +} + +function paRestoreFilters() { + let state; + try { + state = JSON.parse(localStorage.getItem(PA_FILTERS_KEY) || '{}'); + } catch (e) { + return; + } + PA_SAVED_CONTROLS.forEach(id => { + const val = state[id]; + if (typeof val !== 'string') return; + const el = document.getElementById(id); + // A stored option may no longer exist after a UI change - skip it + // rather than leaving the select on a value it cannot display + if (el.tagName === 'SELECT' && ![...el.options].some(o => o.value === val)) return; + el.value = val; + }); + // Sync paFilters, the Clear button and the mobile badge with the + // restored controls before the first render + paReadFilters(); +} + +// Every user-driven filter change goes through here, so the stored set +// only ever reflects deliberate choices - not programmatic ones such as +// the deep-link widening the time range +function paApplyAndSaveFilters() { paApplyFilters(); + paSaveFilters(); } // ================================================================ @@ -1187,16 +1244,19 @@ document.addEventListener('DOMContentLoaded', () => { paDeepLink = { hash: qs.get('hash'), path: qs.get('path') || '' }; } - document.getElementById('paDaysSelect').addEventListener('change', paLoadMessages); + document.getElementById('paDaysSelect').addEventListener('change', () => { + paSaveFilters(); + paLoadMessages(); + }); document.getElementById('paRefreshBtn').addEventListener('click', paLoadMessages); let filterDebounce = null; const debouncedApply = () => { clearTimeout(filterDebounce); - filterDebounce = setTimeout(paApplyFilters, 150); + filterDebounce = setTimeout(paApplyAndSaveFilters, 150); }; - document.getElementById('paHopsFilter').addEventListener('change', paApplyFilters); - document.getElementById('paHashSizeFilter').addEventListener('change', paApplyFilters); + document.getElementById('paHopsFilter').addEventListener('change', paApplyAndSaveFilters); + document.getElementById('paHashSizeFilter').addEventListener('change', paApplyAndSaveFilters); document.getElementById('paTokenFilter').addEventListener('input', debouncedApply); document.getElementById('paSenderFilter').addEventListener('input', debouncedApply); document.getElementById('paContentFilter').addEventListener('input', debouncedApply); @@ -1207,7 +1267,7 @@ document.addEventListener('DOMContentLoaded', () => { document.getElementById('paViewRoutesBtn').addEventListener('click', () => paSwitchView('routes')); document.getElementById('paViewMapBtn').addEventListener('click', () => paSwitchView('map')); document.getElementById('paMapClearBtn').addEventListener('click', paClearMapSelection); - document.getElementById('paSegLenSelect').addEventListener('change', paApplyFilters); + document.getElementById('paSegLenSelect').addEventListener('change', paApplyAndSaveFilters); document.querySelectorAll('#paStatsWrap .pa-sortable').forEach(th => { th.addEventListener('click', () => { const k = th.dataset.sort; @@ -1231,6 +1291,10 @@ document.addEventListener('DOMContentLoaded', () => { }); }); + // A deep link carries its own intent - restoring a saved filter set + // could hide the very message the user clicked through to + if (!paDeepLink) paRestoreFilters(); + paContactsReady = paLoadContacts(); paLoadMessages(); });