diff --git a/CHANGELOG.md b/CHANGELOG.md
index c963f34..e7588eb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,14 @@ every change carries a descriptive commit message.
## [Unreleased]
+## [3.40.2] — 2026-10-26
+
+### Added
+
+- **File filter: reset (✕) button.** A red ✕ to the right of the filter
+ selector appears whenever the filter is not "All" and resets it to "All"
+ in one click.
+
## [3.40.1] — 2026-10-26
### Changed
diff --git a/frontend/index.html b/frontend/index.html
index e6df90a..fbe9b03 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -1127,6 +1127,7 @@
All
+ ✕
Probe all files
@@ -1989,7 +1990,7 @@
// ===== Version =====
// Single source of truth for the PWA version: shown in the header and used
// by the server version check in pysimConnect().
-const SIMPLE_VERSION = '3.40.1';
+const SIMPLE_VERSION = '3.40.2';
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
// ===== view history (the browser Back/Forward buttons) =====
@@ -16270,6 +16271,7 @@ function pysimFsFilterRender() {
const menu = document.getElementById('pysim-fs-filter-menu');
const btn = document.getElementById('pysim-fs-filter-btn');
if (!menu) return;
+ const active = pysimFsFilter.size > 0 && !pysimFsFilter.has(FS_FILTER_ALL);
const services = fsFilterServiceOptions();
const item = (id, label) =>
'' +
@@ -16296,7 +16298,6 @@ function pysimFsFilterRender() {
if (btn) {
btn.textContent = pysimFsFilterSummary();
// a non-"All" selection stands out (blue, like the active sort pill)
- const active = pysimFsFilter.size > 0 && !pysimFsFilter.has(FS_FILTER_ALL);
btn.classList.toggle('bg-blue-600', active);
btn.classList.toggle('text-white', active);
btn.classList.toggle('text-gray-700', !active);
@@ -16304,6 +16305,11 @@ function pysimFsFilterRender() {
btn.classList.toggle('hover:bg-gray-200', !active);
btn.classList.toggle('dark:hover:bg-slate-700', !active);
}
+ const clearBtn = document.getElementById('pysim-fs-filter-clear');
+ if (clearBtn) {
+ clearBtn.classList.toggle('hidden', !active);
+ clearBtn.setAttribute('aria-label', t('Reset filter'));
+ }
}
// Expand or collapse the menu (`force` overrides the toggle; the outside
@@ -16339,6 +16345,11 @@ function pysimFsFilterSetEntry(entry, on) {
pysimFsAutoExpandForFilter();
}
+// The ✕ next to the filter selector: back to "All".
+function pysimFsFilterClear() {
+ pysimFsFilterSetEntry(FS_FILTER_ALL, true);
+}
+
// The cross-reference inverted to service number -> feature name, per file
// table. Both maps are declared later in this file, so they are read on the
// first call, never while the filter block itself is evaluated.
@@ -28100,6 +28111,7 @@ const LANG_RU = {
'Sort:': 'Сортировка:',
'Name': 'Имя',
'Filter:': 'Фильтр:',
+ 'Reset filter': 'Сбросить фильтр',
'(no filters yet)': '(фильтров пока нет)',
'Identifiers': 'Идентификаторы',
'PLMN selection': 'Выбор PLMN',
diff --git a/frontend/sw.js b/frontend/sw.js
index 9725a87..ec983b0 100644
--- a/frontend/sw.js
+++ b/frontend/sw.js
@@ -1,4 +1,4 @@
-const CACHE = 'simple-v486';
+const CACHE = 'simple-v487';
const URLS = [
'index.html',
'help.html',
diff --git a/frontend/tests/fs_filter.test.js b/frontend/tests/fs_filter.test.js
index b71123b..b1e29d4 100644
--- a/frontend/tests/fs_filter.test.js
+++ b/frontend/tests/fs_filter.test.js
@@ -111,10 +111,11 @@ function fakeMenu() {
};
}
-const dom = { menu: null, btn: null };
+const dom = { menu: null, btn: null, clear: null };
globalThis.document = {
getElementById: id => (id === 'pysim-fs-filter-menu' ? dom.menu
- : (id === 'pysim-fs-filter-btn' ? dom.btn : null)),
+ : (id === 'pysim-fs-filter-btn' ? dom.btn
+ : (id === 'pysim-fs-filter-clear' ? dom.clear : null))),
};
let code = '';
@@ -123,7 +124,7 @@ for (const f of ['fsFilterServiceOptions', 'fsFilterIndexBuild', 'fsFilterIndex'
'fsFilterPathsKeyOf', 'pysimFsFilterPathsInvalidate', 'pysimFsFilterMatchingPaths',
'pysimFsFilterMatch', 'pysimFsFilterShow', 'pysimFsFilterLabel',
'pysimFsFilterSummary', 'pysimFsFilterRender', 'pysimFsFilterToggle',
- 'pysimFsFilterSetEntry', 'pysimFsFilterRestore', 'pysimFsNodePath',
+ 'pysimFsFilterSetEntry', 'pysimFsFilterClear', 'pysimFsFilterRestore', 'pysimFsNodePath',
'pysimFsExpandAll', 'pysimFsCollapseAll', 'pysimFsAutoExpandForFilter']) {
code += extractFunc(html, f) + '\n';
}
@@ -161,6 +162,31 @@ test('the file tree has a collapsed filter dropdown next to the sort pills', ()
assert.ok(!/pysimFsSetFilter\(/.test(html));
});
+test('the ✕ next to the selector resets a non-All filter to All', () => {
+ // markup: a hidden red ✕ that clears the filter
+ assert.match(html, /id="pysim-fs-filter-clear"[^>]*onclick="pysimFsFilterClear\(\)"/);
+ assert.match(html, /id="pysim-fs-filter-clear"[^>]*data-l10n-title="Reset filter"/);
+ // the clear handler resets to All
+ const fn = extractFunc(html, 'pysimFsFilterClear');
+ assert.ok(fn.indexOf('pysimFsFilterSetEntry(FS_FILTER_ALL, true)') > 0, fn);
+ // the render shows it only when a non-All filter is active
+ dom.menu = fakeMenu();
+ dom.btn = fakeBtn();
+ dom.clear = fakeBtn();
+ withFilter(['plmn-selection'], () => {
+ pysimFsFilterRender();
+ assert.strictEqual(dom.clear.classes.has('hidden'), false);
+ assert.strictEqual(dom.clear.attrs['aria-label'], 'Reset filter');
+ });
+ withFilter(['all'], () => {
+ pysimFsFilterRender();
+ assert.strictEqual(dom.clear.classes.has('hidden'), true);
+ });
+ // the Russian entry exists
+ const dict = html.match(/const LANG_RU = \{([\s\S]*?)\n\};/)[1];
+ assert.ok(dict.includes("'Reset filter':"), 'missing Russian entry');
+});
+
test('an outside click or Escape closes the menu', () => {
const click = html.match(/document\.addEventListener\('click', e => \{([\s\S]{0,600}?)\n\}\);/);
assert.ok(click, 'no document click listener');
diff --git a/pyproject.toml b/pyproject.toml
index fb78e70..7429f40 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "pysim-simple-server"
-version = "3.40.1"
+version = "3.40.2"
description = "HTTP REST server wrapping pysim for the SIMple PWA"
license = { text = "GPL-2.0-or-later" }
requires-python = ">=3.9"
diff --git a/pysim_simple_server/server.py b/pysim_simple_server/server.py
index c4777f1..e7edd71 100644
--- a/pysim_simple_server/server.py
+++ b/pysim_simple_server/server.py
@@ -37,7 +37,7 @@ from osmocom.tlv import BER_TLV_IE
from osmocom.utils import rpad
-VERSION = '3.40.1'
+VERSION = '3.40.2'
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE