From 69b4fbfb42873ca4e65a82d0f830f7338c5e8672 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 21:54:32 +0300 Subject: [PATCH] profiler editor: show symbolic file names next to rule paths Rules now persist the pySim symbolic name captured during 'Profile from card' (profilerBuildFileRule stores c.name || sel.name), and the profile edit view renders it next to the Path label using the same lookup order as the check view: profilerCustomNameForPath(path) || rule.name. Path edits call the new profilerUpdateRulePath, which drops the stale scan-time name and refreshes the label from the custom-files dictionary live, so the displayed name always corresponds to the current path. Manually added rules get name: null (resolved via custom-file lookup). Old profiles without rule.name still work. 4 new tests (rule.name from child/select/null, custom name lookup, path edit clears name + refreshes label). SW cache v51 -> v52. --- frontend/index.html | 20 ++++++++++++--- frontend/sw.js | 2 +- frontend/tests/profiler.test.js | 43 ++++++++++++++++++++++++++++++++- 3 files changed, 60 insertions(+), 5 deletions(-) diff --git a/frontend/index.html b/frontend/index.html index 3686c31..2eb10fe 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -7196,6 +7196,7 @@ async function profilerBuildFileRule(path, c, ignoreFids, ignoreNames) { const rule = { type: 'file', path: path, + name: c.name || sel.name || null, fileType: sel.file_type || null, fileSize: isRecord ? null : ((sel.file_size === null || sel.file_size === undefined) ? null : sel.file_size), recordLen: (sel.record_len === null || sel.record_len === undefined) ? null : sel.record_len, @@ -7277,7 +7278,7 @@ function profilerBackToList() { function profilerAddRule() { if (!profilerDraft) return; - profilerDraft.rules.push({ type: 'file', path: '', fileType: null, fileSize: null, recordLen: null, numRecords: null, content: null }); + profilerDraft.rules.push({ type: 'file', path: '', name: null, fileType: null, fileSize: null, recordLen: null, numRecords: null, content: null }); profilerRenderEditor(); } @@ -7315,8 +7316,10 @@ function profilerRenderRule(i, r) { html += ''; html += ''; html += '
'; - html += '
' + - '
'; + const sname = profilerCustomNameForPath(r.path) || r.name || ''; + html += '
' + + '
'; html += '
'; const vis = profilerFileFields(r.fileType); html += '
'; @@ -7375,6 +7378,17 @@ function profilerUpdateRule(i, field, value) { profilerDraft.rules[i][field] = value; } +// Path edits drop the scan-time symbolic name (it no longer corresponds); +// a custom-file lookup still supplies a display name for known paths. +function profilerUpdateRulePath(i, value, input) { + if (!profilerDraft) return; + const r = profilerDraft.rules[i]; + r.path = value; + r.name = null; + const span = input && input.parentElement ? input.parentElement.querySelector('.profiler-rule-name') : null; + if (span) span.textContent = profilerCustomNameForPath(value) || ''; +} + function profilerUpdateRuleFileType(i, value) { if (!profilerDraft) return; const r = profilerDraft.rules[i]; diff --git a/frontend/sw.js b/frontend/sw.js index 685d973..e1e6fca 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'otaman-v51'; +const CACHE = 'otaman-v52'; const URLS = [ 'index.html', 'help.html', diff --git a/frontend/tests/profiler.test.js b/frontend/tests/profiler.test.js index 9162bdb..ec96df6 100644 --- a/frontend/tests/profiler.test.js +++ b/frontend/tests/profiler.test.js @@ -21,7 +21,7 @@ function extractFunc(src, name, asyncFn) { return (asyncFn ? 'async ' : '') + src.slice(m.index, i + 1); } -const FNS = ['profilerNormHex', 'profilerMatch', 'profilerMatchMin', 'profilerMaskPrefix4', 'profilerFileFields', 'profilerContentKindForFileType', 'profilerEmptyRecordContent', 'profilerValidateProfile']; +const FNS = ['profilerNormHex', 'profilerMatch', 'profilerMatchMin', 'profilerMaskPrefix4', 'profilerFileFields', 'profilerContentKindForFileType', 'profilerEmptyRecordContent', 'profilerValidateProfile', 'profilerCustomNameForPath', 'profilerUpdateRulePath']; let code = ''; for (const f of FNS) code += extractFunc(html, f) + '\n'; code += extractFunc(html, 'profilerBuildFileRule', true) + '\n'; @@ -156,6 +156,7 @@ test('profilerBuildFileRule skips contents for an ignored FID', async () => { assert.strictEqual(rule.path, 'MF/7F20/6F52'); assert.strictEqual(rule.fileType, 'transparent'); assert.strictEqual(rule.fileSize, 9); + assert.strictEqual(rule.name, 'EF.KcGPRS'); assert.ok(!calls.includes('/api/read'), 'contents must not be read for ignored files'); }); @@ -181,9 +182,49 @@ test('profilerBuildFileRule captures contents for non-ignored files', async () = assert.ok(rule.content); assert.strictEqual(rule.content.mode, 'exact'); assert.strictEqual(rule.content.expected, 'AABBCCDD'); + assert.strictEqual(rule.name, 'EF.ADN'); assert.ok(calls.includes('/api/read')); }); +test('profilerBuildFileRule falls back to the select name when the child has none', async () => { + mockFetch({ '/api/select': () => KCGPRS_SELECT }); + const rule = await profilerBuildFileRule('MF/7F20/6F52', { fid: '6F52' }, new Set(), new Set()); + assert.strictEqual(rule.name, 'EF.KcGPRS'); +}); + +test('profilerBuildFileRule stores null when no symbolic name is known', async () => { + mockFetch({ '/api/select': () => ({ fid: '6F3A', file_type: 'transparent', file_size: 0, exists: true }) }); + const rule = await profilerBuildFileRule('MF/7F20/6F3A', { fid: '6F3A' }, new Set(), new Set()); + assert.strictEqual(rule.name, null); +}); + +test('profilerCustomNameForPath resolves saved custom file names', () => { + global.pysimCustomFiles = [ + { path: '3F00/7F10/6F3A', fid: '6F3A', name: 'My ADN' }, + { path: 'MF/7F20/6F7E', fid: '6F7E', name: 'My LOCI' }, + ]; + assert.strictEqual(profilerCustomNameForPath('MF/7F10/6F3A'), 'My ADN'); + assert.strictEqual(profilerCustomNameForPath('MF/7F20/6F7E'), 'My LOCI'); + assert.strictEqual(profilerCustomNameForPath('MF/7F20/6F3A'), null); + assert.strictEqual(profilerCustomNameForPath(''), null); + assert.strictEqual(profilerCustomNameForPath(null), null); +}); + +test('profilerUpdateRulePath clears the scan-time name and refreshes the label', () => { + global.pysimCustomFiles = [{ path: 'MF/7F10/6F3A', fid: '6F3A', name: 'My ADN' }]; + global.profilerDraft = { rules: [{ path: 'MF/7F20/6F3A', name: 'EF.ADN' }] }; + const span = { textContent: '' }; + const input = { parentElement: { querySelector: () => span } }; + profilerUpdateRulePath(0, 'MF/7F10/6F3A', input); + assert.strictEqual(global.profilerDraft.rules[0].path, 'MF/7F10/6F3A'); + assert.strictEqual(global.profilerDraft.rules[0].name, null); + assert.strictEqual(span.textContent, 'My ADN'); + profilerUpdateRulePath(0, 'MF/7F10/6FB1', input); + assert.strictEqual(span.textContent, ''); + global.profilerDraft = null; + profilerUpdateRulePath(0, 'x', input); +}); + test('profilerBuildFileRule still ignores when ignoreNames is omitted (back-compat)', async () => { const calls = mockFetch({ '/api/select': () => KCGPRS_SELECT }); const rule = await profilerBuildFileRule('MF/7F20/6F52',