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 += '
';
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',