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 <noreply@anthropic.com>
This commit is contained in:
MarekWo
2026-07-26 18:45:17 +02:00
parent f31e4e160c
commit 233b967032
+69 -5
View File
@@ -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();
});