Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6750f72cd5 | ||
|
|
465a43eef2 | ||
|
|
fdbff77188 |
No files matched your search
@@ -9,6 +9,14 @@ every change carries a descriptive commit message.
|
|||||||
|
|
||||||
## [Unreleased]
|
## [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
|
## [3.40.1] — 2026-10-26
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
> Formerly known as OTAMan — renamed to SIMple on 2026-09-19.
|
> Formerly known as OTAMan — renamed to SIMple on 2026-09-19.
|
||||||
|
|
||||||
📢 Project news and updates: [Telegram channel](https://t.me/youeyeseesee)
|
📢 Project news and updates: [website](https://atroshin.ru) · [Telegram channel](https://t.me/youeyeseesee)
|
||||||
|
|
||||||
SIMple is a workbench for UICC/SIM card specialists: an offline HTML/JS PWA plus a local card server ([`pysim-simple-server`](pysim_simple_server/), a pySim/PC-SC bridge that also serves the PWA). It covers the full lab cycle:
|
SIMple is a workbench for UICC/SIM card specialists: an offline HTML/JS PWA plus a local card server ([`pysim-simple-server`](pysim_simple_server/), a pySim/PC-SC bridge that also serves the PWA). It covers the full lab cycle:
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
> Прежнее название — OTAMan; переименован в SIMple 2026-09-19.
|
> Прежнее название — OTAMan; переименован в SIMple 2026-09-19.
|
||||||
|
|
||||||
📢 Новости и обновления проекта: [Telegram-канал](https://t.me/youeyeseesee)
|
📢 Новости и обновления проекта: [сайт](https://atroshin.ru) · [Telegram-канал](https://t.me/youeyeseesee)
|
||||||
|
|
||||||
SIMple — инструментарий для специалистов по UICC/SIM-картам: автономный HTML/JS PWA и локальный сервер карт ([`pysim-simple-server`](pysim_simple_server/) — мост к pySim/PC-SC, он же раздаёт PWA). Закрывает весь цикл лабораторной работы:
|
SIMple — инструментарий для специалистов по UICC/SIM-картам: автономный HTML/JS PWA и локальный сервер карт ([`pysim-simple-server`](pysim_simple_server/) — мост к pySim/PC-SC, он же раздаёт PWA). Закрывает весь цикл лабораторной работы:
|
||||||
|
|
||||||
|
|||||||
+15
-3
@@ -1104,9 +1104,9 @@
|
|||||||
<div id="pysim-body" class="hidden">
|
<div id="pysim-body" class="hidden">
|
||||||
<div class="flex flex-wrap justify-center gap-1 mb-2">
|
<div class="flex flex-wrap justify-center gap-1 mb-2">
|
||||||
<button class="pysim-subtab px-3 py-1 text-sm rounded-full bg-blue-600 text-white" data-pysim-sub="files" data-l10n="File manager">File manager</button>
|
<button class="pysim-subtab px-3 py-1 text-sm rounded-full bg-blue-600 text-white" data-pysim-sub="files" data-l10n="File manager">File manager</button>
|
||||||
|
<button class="pysim-subtab px-3 py-1 text-sm rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600 disabled:opacity-40 disabled:cursor-not-allowed" data-pysim-sub="services" data-needs="card" data-l10n="Services">Services</button>
|
||||||
<button class="pysim-subtab px-3 py-1 text-sm rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600" data-pysim-sub="cmd" data-l10n="pySim command line">pySim command line</button>
|
<button class="pysim-subtab px-3 py-1 text-sm rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600" data-pysim-sub="cmd" data-l10n="pySim command line">pySim command line</button>
|
||||||
<button class="pysim-subtab px-3 py-1 text-sm rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600" data-pysim-sub="apdu" data-l10n="Raw APDU">Raw APDU</button>
|
<button class="pysim-subtab px-3 py-1 text-sm rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600" data-pysim-sub="apdu" data-l10n="Raw APDU">Raw APDU</button>
|
||||||
<button class="pysim-subtab px-3 py-1 text-sm rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600 disabled:opacity-40 disabled:cursor-not-allowed" data-pysim-sub="services" data-needs="card" data-l10n="Services">Services</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="pysim-sub-files">
|
<div id="pysim-sub-files">
|
||||||
@@ -1127,6 +1127,7 @@
|
|||||||
<button type="button" id="pysim-fs-filter-btn" aria-haspopup="true" aria-expanded="false" onclick="pysimFsFilterToggle()" class="px-2 py-0.5 text-xs rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-200 dark:hover:bg-slate-700 text-gray-700 dark:text-slate-300 max-w-xs truncate">All</button>
|
<button type="button" id="pysim-fs-filter-btn" aria-haspopup="true" aria-expanded="false" onclick="pysimFsFilterToggle()" class="px-2 py-0.5 text-xs rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-200 dark:hover:bg-slate-700 text-gray-700 dark:text-slate-300 max-w-xs truncate">All</button>
|
||||||
<div id="pysim-fs-filter-menu" role="group" aria-label="Filter" class="hidden absolute left-0 mt-1 z-50 w-64 max-h-64 overflow-y-auto rounded border border-gray-300 dark:border-slate-600 bg-white dark:bg-slate-800 p-2 shadow-lg"></div>
|
<div id="pysim-fs-filter-menu" role="group" aria-label="Filter" class="hidden absolute left-0 mt-1 z-50 w-64 max-h-64 overflow-y-auto rounded border border-gray-300 dark:border-slate-600 bg-white dark:bg-slate-800 p-2 shadow-lg"></div>
|
||||||
</div>
|
</div>
|
||||||
|
<button type="button" id="pysim-fs-filter-clear" onclick="pysimFsFilterClear()" class="hidden px-1 text-xs text-red-600 dark:text-red-400 hover:text-red-700 dark:hover:text-red-300" data-l10n-title="Reset filter" aria-label="Reset filter">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<button id="pysim-fs-probe-btn" data-needs="card" onclick="pysimFsProbeAll()" class="w-full mb-1 px-2 py-0.5 rounded bg-gray-600 text-white hover:bg-gray-700 disabled:opacity-40 disabled:cursor-not-allowed" data-l10n="Probe all files">Probe all files</button>
|
<button id="pysim-fs-probe-btn" data-needs="card" onclick="pysimFsProbeAll()" class="w-full mb-1 px-2 py-0.5 rounded bg-gray-600 text-white hover:bg-gray-700 disabled:opacity-40 disabled:cursor-not-allowed" data-l10n="Probe all files">Probe all files</button>
|
||||||
<div id="pysim-fs-probe-status" class="hidden mb-1 text-gray-500 dark:text-slate-400"></div>
|
<div id="pysim-fs-probe-status" class="hidden mb-1 text-gray-500 dark:text-slate-400"></div>
|
||||||
@@ -1989,7 +1990,7 @@
|
|||||||
// ===== Version =====
|
// ===== Version =====
|
||||||
// Single source of truth for the PWA version: shown in the header and used
|
// Single source of truth for the PWA version: shown in the header and used
|
||||||
// by the server version check in pysimConnect().
|
// 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;
|
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
|
||||||
|
|
||||||
// ===== view history (the browser Back/Forward buttons) =====
|
// ===== view history (the browser Back/Forward buttons) =====
|
||||||
@@ -16270,6 +16271,7 @@ function pysimFsFilterRender() {
|
|||||||
const menu = document.getElementById('pysim-fs-filter-menu');
|
const menu = document.getElementById('pysim-fs-filter-menu');
|
||||||
const btn = document.getElementById('pysim-fs-filter-btn');
|
const btn = document.getElementById('pysim-fs-filter-btn');
|
||||||
if (!menu) return;
|
if (!menu) return;
|
||||||
|
const active = pysimFsFilter.size > 0 && !pysimFsFilter.has(FS_FILTER_ALL);
|
||||||
const services = fsFilterServiceOptions();
|
const services = fsFilterServiceOptions();
|
||||||
const item = (id, label) =>
|
const item = (id, label) =>
|
||||||
'<label class="flex items-center gap-2 px-2 py-0.5 rounded cursor-pointer hover:bg-gray-100 dark:hover:bg-slate-700 text-xs text-gray-700 dark:text-slate-200">' +
|
'<label class="flex items-center gap-2 px-2 py-0.5 rounded cursor-pointer hover:bg-gray-100 dark:hover:bg-slate-700 text-xs text-gray-700 dark:text-slate-200">' +
|
||||||
@@ -16296,7 +16298,6 @@ function pysimFsFilterRender() {
|
|||||||
if (btn) {
|
if (btn) {
|
||||||
btn.textContent = pysimFsFilterSummary();
|
btn.textContent = pysimFsFilterSummary();
|
||||||
// a non-"All" selection stands out (blue, like the active sort pill)
|
// 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('bg-blue-600', active);
|
||||||
btn.classList.toggle('text-white', active);
|
btn.classList.toggle('text-white', active);
|
||||||
btn.classList.toggle('text-gray-700', !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('hover:bg-gray-200', !active);
|
||||||
btn.classList.toggle('dark:hover:bg-slate-700', !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
|
// Expand or collapse the menu (`force` overrides the toggle; the outside
|
||||||
@@ -16339,6 +16345,11 @@ function pysimFsFilterSetEntry(entry, on) {
|
|||||||
pysimFsAutoExpandForFilter();
|
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
|
// 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
|
// 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.
|
// first call, never while the filter block itself is evaluated.
|
||||||
@@ -28100,6 +28111,7 @@ const LANG_RU = {
|
|||||||
'Sort:': 'Сортировка:',
|
'Sort:': 'Сортировка:',
|
||||||
'Name': 'Имя',
|
'Name': 'Имя',
|
||||||
'Filter:': 'Фильтр:',
|
'Filter:': 'Фильтр:',
|
||||||
|
'Reset filter': 'Сбросить фильтр',
|
||||||
'(no filters yet)': '(фильтров пока нет)',
|
'(no filters yet)': '(фильтров пока нет)',
|
||||||
'Identifiers': 'Идентификаторы',
|
'Identifiers': 'Идентификаторы',
|
||||||
'PLMN selection': 'Выбор PLMN',
|
'PLMN selection': 'Выбор PLMN',
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
const CACHE = 'simple-v486';
|
const CACHE = 'simple-v487';
|
||||||
const URLS = [
|
const URLS = [
|
||||||
'index.html',
|
'index.html',
|
||||||
'help.html',
|
'help.html',
|
||||||
|
|||||||
@@ -111,10 +111,11 @@ function fakeMenu() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const dom = { menu: null, btn: null };
|
const dom = { menu: null, btn: null, clear: null };
|
||||||
globalThis.document = {
|
globalThis.document = {
|
||||||
getElementById: id => (id === 'pysim-fs-filter-menu' ? dom.menu
|
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 = '';
|
let code = '';
|
||||||
@@ -123,7 +124,7 @@ for (const f of ['fsFilterServiceOptions', 'fsFilterIndexBuild', 'fsFilterIndex'
|
|||||||
'fsFilterPathsKeyOf', 'pysimFsFilterPathsInvalidate', 'pysimFsFilterMatchingPaths',
|
'fsFilterPathsKeyOf', 'pysimFsFilterPathsInvalidate', 'pysimFsFilterMatchingPaths',
|
||||||
'pysimFsFilterMatch', 'pysimFsFilterShow', 'pysimFsFilterLabel',
|
'pysimFsFilterMatch', 'pysimFsFilterShow', 'pysimFsFilterLabel',
|
||||||
'pysimFsFilterSummary', 'pysimFsFilterRender', 'pysimFsFilterToggle',
|
'pysimFsFilterSummary', 'pysimFsFilterRender', 'pysimFsFilterToggle',
|
||||||
'pysimFsFilterSetEntry', 'pysimFsFilterRestore', 'pysimFsNodePath',
|
'pysimFsFilterSetEntry', 'pysimFsFilterClear', 'pysimFsFilterRestore', 'pysimFsNodePath',
|
||||||
'pysimFsExpandAll', 'pysimFsCollapseAll', 'pysimFsAutoExpandForFilter']) {
|
'pysimFsExpandAll', 'pysimFsCollapseAll', 'pysimFsAutoExpandForFilter']) {
|
||||||
code += extractFunc(html, f) + '\n';
|
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));
|
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', () => {
|
test('an outside click or Escape closes the menu', () => {
|
||||||
const click = html.match(/document\.addEventListener\('click', e => \{([\s\S]{0,600}?)\n\}\);/);
|
const click = html.match(/document\.addEventListener\('click', e => \{([\s\S]{0,600}?)\n\}\);/);
|
||||||
assert.ok(click, 'no document click listener');
|
assert.ok(click, 'no document click listener');
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "pysim-simple-server"
|
name = "pysim-simple-server"
|
||||||
version = "3.40.1"
|
version = "3.40.2"
|
||||||
description = "HTTP REST server wrapping pysim for the SIMple PWA"
|
description = "HTTP REST server wrapping pysim for the SIMple PWA"
|
||||||
license = { text = "GPL-2.0-or-later" }
|
license = { text = "GPL-2.0-or-later" }
|
||||||
requires-python = ">=3.9"
|
requires-python = ">=3.9"
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ from osmocom.tlv import BER_TLV_IE
|
|||||||
from osmocom.utils import rpad
|
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
|
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user