From d9b2cb7db25318f045666faf1a18d9d2ac3cf089 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D1=82=D0=BE=D0=BD=20=D0=A2=D1=80=D0=BE=D1=88?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Thu, 10 Sep 2026 22:26:30 +0300 Subject: [PATCH] profiler: show scan progress during 'Profile from card' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Split profilerScanCard into two phases so the file total is known up front: phase 1 walks the filesystem (tree calls only) collecting every file entry, phase 2 builds a rule per file. The optional onProgress(done, total, path) callback reports 'N / total files (%) — path'; the scan dialog shows it under the FCP/FCI selector, starting with 'Discovering…' during phase 1. No extra card I/O (tree calls are no longer interleaved with select/read). Backward compatible: onProgress is optional. 2 new tests (progress sequence 0/total then 1..total with paths; scan without callback). RU i18n keys (Discovering..., files). Help docs synced. SW cache v53 -> v54. --- frontend/help-ru.html | 2 +- frontend/help.html | 2 +- frontend/index.html | 31 ++++++++++++++++--- frontend/sw.js | 2 +- frontend/tests/profiler.test.js | 55 +++++++++++++++++++++++++++++++++ 5 files changed, 84 insertions(+), 8 deletions(-) diff --git a/frontend/help-ru.html b/frontend/help-ru.html index eabeb07..e6ebc62 100644 --- a/frontend/help-ru.html +++ b/frontend/help-ru.html @@ -381,7 +381,7 @@

Check выполняет каждое правило на подключённой карте и показывает строку прогресса и отчёт прохождения (существование, каждый атрибут FCP и совпадение содержимого).

Опции сканирования «Profile from card»

-

Диалог сканирования запрашивает имя профиля и предлагает селектор «FCP/FCI check» (те же три режима, по умолчанию Filetype + size), применяемый ко всем создаваемым правилам, а также список «Ignore contents of» (все отмечены по умолчанию) часто перезаписываемых файлов, содержимое которых пропускается: EF.LOCI, EF.PSLOCI, EF.EPSLOCI, EF.5GS3GPPLOCI, EF.Keys, EF.KeysPS, EF.SMS, EF.Kc, EF.KcGPRS, EF.LOCIGPRS, EF.CBMID, EF.SMSS. Правила создаются только для файлов, которые реально существуют на карте (возвращён FCP-шаблон); отсутствующие файлы пропускаются. Пользовательские файлы из подвкладки Custom files включаются с той же проверкой существования.

+

Диалог сканирования запрашивает имя профиля и предлагает селектор «FCP/FCI check» (те же три режима, по умолчанию Filetype + size), применяемый ко всем создаваемым правилам, а также список «Ignore contents of» (все отмечены по умолчанию) часто перезаписываемых файлов, содержимое которых пропускается: EF.LOCI, EF.PSLOCI, EF.EPSLOCI, EF.5GS3GPPLOCI, EF.Keys, EF.KeysPS, EF.SMS, EF.Kc, EF.KcGPRS, EF.LOCIGPRS, EF.CBMID, EF.SMSS. Строка прогресса показывает N / всего файлов (%) во время сканирования. Правила создаются только для файлов, которые реально существуют на карте (возвращён FCP-шаблон); отсутствующие файлы пропускаются. Пользовательские файлы из подвкладки Custom files включаются с той же проверкой существования.

5.7 Сценарии использования

diff --git a/frontend/help.html b/frontend/help.html index bf9ee27..03fbfd7 100644 --- a/frontend/help.html +++ b/frontend/help.html @@ -381,7 +381,7 @@

Check runs every rule against the equipped card and shows a live progress line plus a pass/fail report (existence, each FCI attribute, and the content match).

“Profile from card” scan options

-

The scan dialog asks for a profile name and offers a “FCP/FCI check” selector (the same three modes above, default Filetype + size) applied to every generated rule, plus an “Ignore contents of” checklist (all checked by default) of frequently-overwritten files whose contents are skipped: EF.LOCI, EF.PSLOCI, EF.EPSLOCI, EF.5GS3GPPLOCI, EF.Keys, EF.KeysPS, EF.SMS, EF.Kc, EF.KcGPRS, EF.LOCIGPRS, EF.CBMID, EF.SMSS. Rules are created only for files that actually exist on the card (a FCP template is returned); missing files are skipped. Custom files from the Custom files sub-tab are included under the same existence check.

+

The scan dialog asks for a profile name and offers a “FCP/FCI check” selector (the same three modes above, default Filetype + size) applied to every generated rule, plus an “Ignore contents of” checklist (all checked by default) of frequently-overwritten files whose contents are skipped: EF.LOCI, EF.PSLOCI, EF.EPSLOCI, EF.5GS3GPPLOCI, EF.Keys, EF.KeysPS, EF.SMS, EF.Kc, EF.KcGPRS, EF.LOCIGPRS, EF.CBMID, EF.SMSS. A progress line shows N / total files (%) while scanning. Rules are created only for files that actually exist on the card (a FCP template is returned); missing files are skipped. Custom files from the Custom files sub-tab are included under the same existence check.

5.7 Usage scenarios

diff --git a/frontend/index.html b/frontend/index.html index eed10e6..c620f25 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -920,6 +920,7 @@ +
@@ -7128,10 +7129,16 @@ async function profilerScanStart() { }); const fciMode = document.getElementById('profiler-scan-fci-mode').value; const btn = document.getElementById('profiler-scan-btn'); + const progEl = document.getElementById('profiler-scan-progress'); + progEl.classList.remove('hidden'); + progEl.textContent = t('Discovering...'); btn.disabled = true; btn.textContent = t('Scanning...'); try { - const rules = await profilerScanCard(ignoreFids, ignoreNames, fciMode); + const rules = await profilerScanCard(ignoreFids, ignoreNames, fciMode, (done, total, path) => { + const pct = total ? Math.round(done / total * 100) : 0; + progEl.textContent = done + ' / ' + total + ' ' + t('files') + ' (' + pct + '%)' + (path ? ' — ' + path : ''); + }); const profile = { id: profilerNewId(), name: name, created: new Date().toISOString(), rules: rules }; profiles.push(profile); profilerSave(); @@ -7141,14 +7148,19 @@ async function profilerScanStart() { errEl.textContent = e.message; errEl.classList.remove('hidden'); } finally { + progEl.classList.add('hidden'); btn.disabled = false; btn.textContent = t('Scan'); } } // Recursively walk the card filesystem and build rules for files that exist. -async function profilerScanCard(ignoreFids, ignoreNames, fciMode) { +// Phase 1 discovers every file (tree calls only) to know the total; phase 2 +// builds a rule per file, reporting progress via the optional onProgress +// callback as onProgress(done, total, path). +async function profilerScanCard(ignoreFids, ignoreNames, fciMode, onProgress) { const rules = []; + const files = []; const seen = new Set(); // dir: { name, fid, parentSel (to select this dir), pathPrefix (rule-path segs for its children) } @@ -7174,8 +7186,7 @@ async function profilerScanCard(ignoreFids, ignoreNames, fciMode) { const childPath = dir.pathPrefix.concat(c.fid ? c.fid.toUpperCase() : c.name).join('/'); if (seen.has(childPath)) continue; seen.add(childPath); - const rule = await profilerBuildFileRule(childPath, c, ignoreFids, ignoreNames, fciMode); - if (rule) rules.push(rule); + files.push({ path: childPath, child: c }); } } } @@ -7193,7 +7204,15 @@ async function profilerScanCard(ignoreFids, ignoreNames, fciMode) { const path = segs.join('/'); if (seen.has(path)) continue; seen.add(path); - const rule = await profilerBuildFileRule(path, { fid: cf.fid, name: cf.name }, ignoreFids, ignoreNames, fciMode); + files.push({ path: path, child: { fid: cf.fid, name: cf.name } }); + } + + const total = files.length; + if (onProgress) onProgress(0, total, ''); + for (let i = 0; i < files.length; i++) { + const entry = files[i]; + if (onProgress) onProgress(i + 1, total, entry.path); + const rule = await profilerBuildFileRule(entry.path, entry.child, ignoreFids, ignoreNames, fciMode); if (rule) rules.push(rule); } return rules; @@ -7847,6 +7866,8 @@ const LANG_RU = { 'Back to list': 'Назад к списку', 'Add rule': 'Добавить правило', 'Scan': 'Сканировать', + 'Discovering...': 'Поиск файлов...', + 'files': 'файлов', 'Ignore contents of:': 'Игнорировать содержимое:', 'Ignore contents of': 'Игнорировать содержимое', 'FCP/FCI check': 'Проверка FCP/FCI', diff --git a/frontend/sw.js b/frontend/sw.js index 964b6f8..6e0625a 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'otaman-v53'; +const CACHE = 'otaman-v54'; const URLS = [ 'index.html', 'help.html', diff --git a/frontend/tests/profiler.test.js b/frontend/tests/profiler.test.js index b54ba5f..329fb14 100644 --- a/frontend/tests/profiler.test.js +++ b/frontend/tests/profiler.test.js @@ -26,6 +26,7 @@ let code = ''; for (const f of FNS) code += extractFunc(html, f) + '\n'; code += extractFunc(html, 'profilerBuildFileRule', true) + '\n'; code += extractFunc(html, 'profilerRunRule', true) + '\n'; +code += extractFunc(html, 'profilerScanCard', true) + '\n'; code += html.match(/const PROFILER_MASK_PREFIX4_FIDS = \{[\s\S]*?\n\};/)[0] + '\n'; eval(code); @@ -310,3 +311,57 @@ test('profilerValidateProfile accepts valid fciMode and rejects unknown', () => assert.strictEqual(profilerValidateProfile({ name: 'x', rules: [{ type: 'file', path: 'MF/6F07', fciMode: 'exact' }] }), null); assert.strictEqual(profilerValidateProfile({ name: 'x', rules: [{ type: 'file', path: 'MF/6F07', fciMode: 'bogus' }] }), 'Invalid FCP/FCI mode: bogus'); }); + +// --- scan progress --- + +test('profilerScanCard reports discovery total then per-file progress', async () => { + const SEL = (fid, name) => ({ name, fid, file_type: 'transparent', file_size: 4, record_len: null, num_of_rec: null, exists: true }); + const tree = { + MF: { exists: true, name: 'MF', children: [ + { name: 'DF.GSM', fid: '7f20', isDir: true }, + { name: 'EF.IMSI', fid: '6f07', isDir: false }, + ] }, + 'DF.GSM': { exists: true, name: 'DF.GSM', children: [ + { name: 'EF.ADN', fid: '6f3a', isDir: false }, + { name: 'EF.LOCI', fid: '6f7e', isDir: false }, + ] }, + }; + global.pysimCustomFiles = [{ path: 'MF/7F10/6F3A', fid: '6F3A', name: 'My ADN' }]; + global.pysimFetch = async (path, body) => { + if (path === '/api/tree') { + const key = body.name === 'DF.GSM' ? 'DF.GSM' : 'MF'; + return tree[key]; + } + if (path === '/api/select') { + const fid = body.path.split('/').pop(); + return SEL(fid, 'EF.' + fid); + } + throw new Error('unexpected fetch: ' + path); + }; + + const progress = []; + const rules = await profilerScanCard(new Set(), new Set(), 'type', (done, total, path) => { + progress.push([done, total, path]); + }); + + assert.strictEqual(rules.length, 4); + // initial 0/total report + assert.deepStrictEqual(progress[0], [0, 4, '']); + // then 1..4 with the file path, in scan order + assert.strictEqual(progress.length, 5); + assert.strictEqual(progress[4][0], 4); + assert.strictEqual(progress[4][1], 4); + const paths = progress.slice(1).map(p => p[2]); + assert.deepStrictEqual(paths, ['MF/7F20/6F3A', 'MF/7F20/6F7E', 'MF/6F07', 'MF/7F10/6F3A']); +}); + +test('profilerScanCard without onProgress still works (back-compat)', async () => { + global.pysimCustomFiles = []; + global.pysimFetch = async (path, body) => { + if (path === '/api/tree') return { exists: true, name: 'MF', children: [{ name: 'EF.IMSI', fid: '6f07', isDir: false }] }; + if (path === '/api/select') return { name: 'EF.IMSI', fid: '6F07', file_type: 'transparent', file_size: 9, record_len: null, num_of_rec: null, exists: true }; + throw new Error('unexpected fetch: ' + path); + }; + const rules = await profilerScanCard(new Set(), new Set(), 'type_size'); + assert.strictEqual(rules.length, 1); +});