profiler: show scan progress during 'Profile from card'
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.
This commit is contained in:
@@ -381,7 +381,7 @@
|
|||||||
</ul>
|
</ul>
|
||||||
<p class="text-sm mb-3"><strong>Check</strong> выполняет каждое правило на подключённой карте и показывает строку прогресса и отчёт прохождения (существование, каждый атрибут FCP и совпадение содержимого).</p>
|
<p class="text-sm mb-3"><strong>Check</strong> выполняет каждое правило на подключённой карте и показывает строку прогресса и отчёт прохождения (существование, каждый атрибут FCP и совпадение содержимого).</p>
|
||||||
<h4 class="font-medium mb-1">Опции сканирования «Profile from card»</h4>
|
<h4 class="font-medium mb-1">Опции сканирования «Profile from card»</h4>
|
||||||
<p class="text-sm mb-2">Диалог сканирования запрашивает имя профиля и предлагает селектор <strong>«FCP/FCI check»</strong> (те же три режима, по умолчанию <strong>Filetype + size</strong>), применяемый ко всем создаваемым правилам, а также список <strong>«Ignore contents of»</strong> (все отмечены по умолчанию) часто перезаписываемых файлов, содержимое которых пропускается: <code class="font-mono text-sm">EF.LOCI</code>, <code class="font-mono text-sm">EF.PSLOCI</code>, <code class="font-mono text-sm">EF.EPSLOCI</code>, <code class="font-mono text-sm">EF.5GS3GPPLOCI</code>, <code class="font-mono text-sm">EF.Keys</code>, <code class="font-mono text-sm">EF.KeysPS</code>, <code class="font-mono text-sm">EF.SMS</code>, <code class="font-mono text-sm">EF.Kc</code>, <code class="font-mono text-sm">EF.KcGPRS</code>, <code class="font-mono text-sm">EF.LOCIGPRS</code>, <code class="font-mono text-sm">EF.CBMID</code>, <code class="font-mono text-sm">EF.SMSS</code>. Правила создаются только для файлов, которые реально существуют на карте (возвращён FCP-шаблон); отсутствующие файлы пропускаются. Пользовательские файлы из подвкладки <strong>Custom files</strong> включаются с той же проверкой существования.</p>
|
<p class="text-sm mb-2">Диалог сканирования запрашивает имя профиля и предлагает селектор <strong>«FCP/FCI check»</strong> (те же три режима, по умолчанию <strong>Filetype + size</strong>), применяемый ко всем создаваемым правилам, а также список <strong>«Ignore contents of»</strong> (все отмечены по умолчанию) часто перезаписываемых файлов, содержимое которых пропускается: <code class="font-mono text-sm">EF.LOCI</code>, <code class="font-mono text-sm">EF.PSLOCI</code>, <code class="font-mono text-sm">EF.EPSLOCI</code>, <code class="font-mono text-sm">EF.5GS3GPPLOCI</code>, <code class="font-mono text-sm">EF.Keys</code>, <code class="font-mono text-sm">EF.KeysPS</code>, <code class="font-mono text-sm">EF.SMS</code>, <code class="font-mono text-sm">EF.Kc</code>, <code class="font-mono text-sm">EF.KcGPRS</code>, <code class="font-mono text-sm">EF.LOCIGPRS</code>, <code class="font-mono text-sm">EF.CBMID</code>, <code class="font-mono text-sm">EF.SMSS</code>. Строка прогресса показывает <em>N / всего файлов (%)</em> во время сканирования. Правила создаются только для файлов, которые реально существуют на карте (возвращён FCP-шаблон); отсутствующие файлы пропускаются. Пользовательские файлы из подвкладки <strong>Custom files</strong> включаются с той же проверкой существования.</p>
|
||||||
|
|
||||||
<h3 id="usage-scenarios" class="text-lg font-medium mb-2">5.7 Сценарии использования</h3>
|
<h3 id="usage-scenarios" class="text-lg font-medium mb-2">5.7 Сценарии использования</h3>
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -381,7 +381,7 @@
|
|||||||
</ul>
|
</ul>
|
||||||
<p class="text-sm mb-3"><strong>Check</strong> 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).</p>
|
<p class="text-sm mb-3"><strong>Check</strong> 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).</p>
|
||||||
<h4 class="font-medium mb-1">“Profile from card” scan options</h4>
|
<h4 class="font-medium mb-1">“Profile from card” scan options</h4>
|
||||||
<p class="text-sm mb-2">The scan dialog asks for a profile name and offers a <strong>“FCP/FCI check”</strong> selector (the same three modes above, default <strong>Filetype + size</strong>) applied to every generated rule, plus an <strong>“Ignore contents of”</strong> checklist (all checked by default) of frequently-overwritten files whose contents are skipped: <code class="font-mono text-sm">EF.LOCI</code>, <code class="font-mono text-sm">EF.PSLOCI</code>, <code class="font-mono text-sm">EF.EPSLOCI</code>, <code class="font-mono text-sm">EF.5GS3GPPLOCI</code>, <code class="font-mono text-sm">EF.Keys</code>, <code class="font-mono text-sm">EF.KeysPS</code>, <code class="font-mono text-sm">EF.SMS</code>, <code class="font-mono text-sm">EF.Kc</code>, <code class="font-mono text-sm">EF.KcGPRS</code>, <code class="font-mono text-sm">EF.LOCIGPRS</code>, <code class="font-mono text-sm">EF.CBMID</code>, <code class="font-mono text-sm">EF.SMSS</code>. 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 <strong>Custom files</strong> sub-tab are included under the same existence check.</p>
|
<p class="text-sm mb-2">The scan dialog asks for a profile name and offers a <strong>“FCP/FCI check”</strong> selector (the same three modes above, default <strong>Filetype + size</strong>) applied to every generated rule, plus an <strong>“Ignore contents of”</strong> checklist (all checked by default) of frequently-overwritten files whose contents are skipped: <code class="font-mono text-sm">EF.LOCI</code>, <code class="font-mono text-sm">EF.PSLOCI</code>, <code class="font-mono text-sm">EF.EPSLOCI</code>, <code class="font-mono text-sm">EF.5GS3GPPLOCI</code>, <code class="font-mono text-sm">EF.Keys</code>, <code class="font-mono text-sm">EF.KeysPS</code>, <code class="font-mono text-sm">EF.SMS</code>, <code class="font-mono text-sm">EF.Kc</code>, <code class="font-mono text-sm">EF.KcGPRS</code>, <code class="font-mono text-sm">EF.LOCIGPRS</code>, <code class="font-mono text-sm">EF.CBMID</code>, <code class="font-mono text-sm">EF.SMSS</code>. A progress line shows <em>N / total files (%)</em> 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 <strong>Custom files</strong> sub-tab are included under the same existence check.</p>
|
||||||
|
|
||||||
<h3 id="usage-scenarios" class="text-lg font-medium mb-2">5.7 Usage scenarios</h3>
|
<h3 id="usage-scenarios" class="text-lg font-medium mb-2">5.7 Usage scenarios</h3>
|
||||||
|
|
||||||
|
|||||||
+26
-5
@@ -920,6 +920,7 @@
|
|||||||
<option value="type_size" selected data-l10n="Filetype + size (FCP)">Filetype + size (FCP)</option>
|
<option value="type_size" selected data-l10n="Filetype + size (FCP)">Filetype + size (FCP)</option>
|
||||||
<option value="exact" data-l10n="Exact FCI">Exact FCI</option>
|
<option value="exact" data-l10n="Exact FCI">Exact FCI</option>
|
||||||
</select>
|
</select>
|
||||||
|
<div id="profiler-scan-progress" class="text-xs text-gray-500 dark:text-slate-400 mb-2 hidden"></div>
|
||||||
<div id="profiler-scan-error" class="text-xs text-red-500 hidden mb-2"></div>
|
<div id="profiler-scan-error" class="text-xs text-red-500 hidden mb-2"></div>
|
||||||
<div class="flex gap-2 justify-end">
|
<div class="flex gap-2 justify-end">
|
||||||
<button onclick="profilerScanCancel()" class="px-3 py-1.5 text-xs rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-100 dark:hover:bg-slate-700 text-gray-700 dark:text-slate-300" data-l10n="Cancel">Cancel</button>
|
<button onclick="profilerScanCancel()" class="px-3 py-1.5 text-xs rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-100 dark:hover:bg-slate-700 text-gray-700 dark:text-slate-300" data-l10n="Cancel">Cancel</button>
|
||||||
@@ -7128,10 +7129,16 @@ async function profilerScanStart() {
|
|||||||
});
|
});
|
||||||
const fciMode = document.getElementById('profiler-scan-fci-mode').value;
|
const fciMode = document.getElementById('profiler-scan-fci-mode').value;
|
||||||
const btn = document.getElementById('profiler-scan-btn');
|
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.disabled = true;
|
||||||
btn.textContent = t('Scanning...');
|
btn.textContent = t('Scanning...');
|
||||||
try {
|
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 };
|
const profile = { id: profilerNewId(), name: name, created: new Date().toISOString(), rules: rules };
|
||||||
profiles.push(profile);
|
profiles.push(profile);
|
||||||
profilerSave();
|
profilerSave();
|
||||||
@@ -7141,14 +7148,19 @@ async function profilerScanStart() {
|
|||||||
errEl.textContent = e.message;
|
errEl.textContent = e.message;
|
||||||
errEl.classList.remove('hidden');
|
errEl.classList.remove('hidden');
|
||||||
} finally {
|
} finally {
|
||||||
|
progEl.classList.add('hidden');
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.textContent = t('Scan');
|
btn.textContent = t('Scan');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recursively walk the card filesystem and build rules for files that exist.
|
// 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 rules = [];
|
||||||
|
const files = [];
|
||||||
const seen = new Set();
|
const seen = new Set();
|
||||||
|
|
||||||
// dir: { name, fid, parentSel (to select this dir), pathPrefix (rule-path segs for its children) }
|
// 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('/');
|
const childPath = dir.pathPrefix.concat(c.fid ? c.fid.toUpperCase() : c.name).join('/');
|
||||||
if (seen.has(childPath)) continue;
|
if (seen.has(childPath)) continue;
|
||||||
seen.add(childPath);
|
seen.add(childPath);
|
||||||
const rule = await profilerBuildFileRule(childPath, c, ignoreFids, ignoreNames, fciMode);
|
files.push({ path: childPath, child: c });
|
||||||
if (rule) rules.push(rule);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -7193,7 +7204,15 @@ async function profilerScanCard(ignoreFids, ignoreNames, fciMode) {
|
|||||||
const path = segs.join('/');
|
const path = segs.join('/');
|
||||||
if (seen.has(path)) continue;
|
if (seen.has(path)) continue;
|
||||||
seen.add(path);
|
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);
|
if (rule) rules.push(rule);
|
||||||
}
|
}
|
||||||
return rules;
|
return rules;
|
||||||
@@ -7847,6 +7866,8 @@ const LANG_RU = {
|
|||||||
'Back to list': 'Назад к списку',
|
'Back to list': 'Назад к списку',
|
||||||
'Add rule': 'Добавить правило',
|
'Add rule': 'Добавить правило',
|
||||||
'Scan': 'Сканировать',
|
'Scan': 'Сканировать',
|
||||||
|
'Discovering...': 'Поиск файлов...',
|
||||||
|
'files': 'файлов',
|
||||||
'Ignore contents of:': 'Игнорировать содержимое:',
|
'Ignore contents of:': 'Игнорировать содержимое:',
|
||||||
'Ignore contents of': 'Игнорировать содержимое',
|
'Ignore contents of': 'Игнорировать содержимое',
|
||||||
'FCP/FCI check': 'Проверка FCP/FCI',
|
'FCP/FCI check': 'Проверка FCP/FCI',
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
const CACHE = 'otaman-v53';
|
const CACHE = 'otaman-v54';
|
||||||
const URLS = [
|
const URLS = [
|
||||||
'index.html',
|
'index.html',
|
||||||
'help.html',
|
'help.html',
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ let code = '';
|
|||||||
for (const f of FNS) code += extractFunc(html, f) + '\n';
|
for (const f of FNS) code += extractFunc(html, f) + '\n';
|
||||||
code += extractFunc(html, 'profilerBuildFileRule', true) + '\n';
|
code += extractFunc(html, 'profilerBuildFileRule', true) + '\n';
|
||||||
code += extractFunc(html, 'profilerRunRule', 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';
|
code += html.match(/const PROFILER_MASK_PREFIX4_FIDS = \{[\s\S]*?\n\};/)[0] + '\n';
|
||||||
eval(code);
|
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: 'exact' }] }), null);
|
||||||
assert.strictEqual(profilerValidateProfile({ name: 'x', rules: [{ type: 'file', path: 'MF/6F07', fciMode: 'bogus' }] }), 'Invalid FCP/FCI mode: bogus');
|
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);
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user