Compare commits

...

3 Commits

Author SHA1 Message Date
catarrh 30969f8f04 profiler editor: keep record/content fields visible
- Seed one empty record row when record content is created or reset
  (filetype change or Contents None->Exact/Mask) via profilerEmptyRecordContent
- Add per-record delete and '+ Record' add controls so the record view is
  always editable; delete is hidden on the last remaining row

SW cache v47.
2026-09-09 23:46:04 +03:00
catarrh 569f308cec profiler: skip fileSize for linear-fixed/cyclic files
- Do not store fileSize on record-file rules during 'Profile from card'
- Do not check/report fileSize mismatch for record-file rules; only
  recordLen and numRecords are reported for LF/CY files

SW cache v46.
2026-09-09 23:35:21 +03:00
catarrh 61ab7bc304 profiler editor: filetype-driven fields and content view
- Show Size only for transparent/ber_tlv; Record length/count only for
  linear_fixed/cyclic; hide all size/record fields for df (unknown shows all)
- Hide the Contents section entirely for df (existence-only check)
- Changing file type clears now-inapplicable fields and resets content
  (keeps mode) when its kind switches between record and transparent
- profilerSetContent defaults content kind from the selected file type

Add profilerFileFields + profilerContentKindForFileType helpers + tests.
SW cache v45.
2026-09-09 23:24:18 +03:00
3 changed files with 111 additions and 14 deletions
+83 -12
View File
@@ -6731,6 +6731,24 @@ function profilerMaskPrefix4(hex) {
return h.slice(0, 8) + '?'.repeat(h.length - 8);
}
// Which size/record fields apply to a given file type.
function profilerFileFields(ft) {
if (ft === 'linear_fixed' || ft === 'cyclic') return { size: false, records: true };
if (ft === 'df') return { size: false, records: false };
if (ft === 'transparent' || ft === 'ber_tlv') return { size: true, records: false };
return { size: true, records: true };
}
// Content representation ('record' vs 'transparent') for a given file type.
function profilerContentKindForFileType(ft) {
return (ft === 'linear_fixed' || ft === 'cyclic') ? 'record' : 'transparent';
}
// A fresh record content object seeded with one empty record row.
function profilerEmptyRecordContent(mode) {
return { mode: mode, kind: 'record', records: [{ num: 1, data: '' }] };
}
function profilerValidateProfile(obj) {
if (!obj || typeof obj !== 'object') return 'Not an object';
if (!obj.name) return 'Missing name';
@@ -6890,11 +6908,12 @@ async function profilerBuildFileRule(path, c, ignoreFids) {
sel = await pysimFetch('/api/select', { path: path });
} catch (e) { return null; }
if (!sel || sel.exists !== true) return null;
const isRecord = profilerContentKindForFileType(sel.file_type) === 'record';
const rule = {
type: 'file',
path: path,
fileType: sel.file_type || null,
fileSize: (sel.file_size === null || sel.file_size === undefined) ? null : sel.file_size,
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,
numRecords: (sel.num_of_rec === null || sel.num_of_rec === undefined) ? null : sel.num_of_rec,
content: null,
@@ -7014,17 +7033,24 @@ function profilerRenderRule(i, r) {
html += '<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">' + esc(t('Path')) + '</label>' +
'<input value="' + escHtml(r.path || '') + '" oninput="profilerUpdateRule(' + i + ',\'path\',this.value)" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" placeholder="MF/7F10/6F3A"></div>';
html += '</div>';
const vis = profilerFileFields(r.fileType);
html += '<div class="grid grid-cols-2 md:grid-cols-4 gap-2 mb-2">';
html += '<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">' + esc(t('File type')) + '</label>' +
'<select onchange="profilerUpdateRule(' + i + ',\'fileType\',this.value||null)" class="w-full border border-gray-300 dark:border-slate-600 text-xs rounded px-2 py-1 dark:bg-slate-800">' + ftOpts + '</select></div>';
html += '<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">' + esc(t('Size')) + '</label>' +
'<input type="number" value="' + (r.fileSize === null ? '' : r.fileSize) + '" oninput="profilerUpdateRule(' + i + ',\'fileSize\',this.value===\'\'?null:parseInt(this.value))" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" placeholder="—"></div>';
html += '<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">' + esc(t('Record length')) + '</label>' +
'<input type="number" value="' + (r.recordLen === null ? '' : r.recordLen) + '" oninput="profilerUpdateRule(' + i + ',\'recordLen\',this.value===\'\'?null:parseInt(this.value))" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" placeholder="—"></div>';
html += '<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">' + esc(t('Record count')) + '</label>' +
'<input type="number" value="' + (r.numRecords === null ? '' : r.numRecords) + '" oninput="profilerUpdateRule(' + i + ',\'numRecords\',this.value===\'\'?null:parseInt(this.value))" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" placeholder="—"></div>';
'<select onchange="profilerUpdateRuleFileType(' + i + ',this.value||null)" class="w-full border border-gray-300 dark:border-slate-600 text-xs rounded px-2 py-1 dark:bg-slate-800">' + ftOpts + '</select></div>';
if (vis.size) {
html += '<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">' + esc(t('Size')) + '</label>' +
'<input type="number" value="' + (r.fileSize === null ? '' : r.fileSize) + '" oninput="profilerUpdateRule(' + i + ',\'fileSize\',this.value===\'\'?null:parseInt(this.value))" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" placeholder="—"></div>';
}
if (vis.records) {
html += '<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">' + esc(t('Record length')) + '</label>' +
'<input type="number" value="' + (r.recordLen === null ? '' : r.recordLen) + '" oninput="profilerUpdateRule(' + i + ',\'recordLen\',this.value===\'\'?null:parseInt(this.value))" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" placeholder="—"></div>';
html += '<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">' + esc(t('Record count')) + '</label>' +
'<input type="number" value="' + (r.numRecords === null ? '' : r.numRecords) + '" oninput="profilerUpdateRule(' + i + ',\'numRecords\',this.value===\'\'?null:parseInt(this.value))" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" placeholder="—"></div>';
}
html += '</div>';
html += profilerRenderContentEditor(i, r);
if (r.fileType !== 'df') {
html += profilerRenderContentEditor(i, r);
}
html += '</div>';
return html;
}
@@ -7044,9 +7070,14 @@ function profilerRenderContentEditor(i, r) {
for (let ri = 0; ri < c.records.length; ri++) {
const rec = c.records[ri];
html += '<div class="flex items-center gap-2"><span class="text-xs text-gray-500 w-8">' + rec.num + '</span>' +
'<input value="' + escHtml(rec.data) + '" oninput="profilerUpdateRecord(' + i + ',' + ri + ',this.value)" class="flex-1 font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800"></div>';
'<input value="' + escHtml(rec.data) + '" oninput="profilerUpdateRecord(' + i + ',' + ri + ',this.value)" class="flex-1 font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800">';
if (c.records.length > 1) {
html += '<button onclick="profilerDeleteRecord(' + i + ',' + ri + ')" class="text-red-400 hover:text-red-600 text-xs px-1"></button>';
}
html += '</div>';
}
html += '</div>';
html += '<button onclick="profilerAddRecord(' + i + ')" class="mt-1 text-xs px-2 py-0.5 rounded bg-gray-200 dark:bg-slate-700 hover:bg-gray-300 dark:hover:bg-slate-600 text-gray-700 dark:text-slate-300">+ Record</button>';
} else {
html += '<textarea oninput="profilerUpdateRuleContent(' + i + ',this.value)" rows="2" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800">' + escHtml(c.expected || '') + '</textarea>';
}
@@ -7059,6 +7090,24 @@ function profilerUpdateRule(i, field, value) {
profilerDraft.rules[i][field] = value;
}
function profilerUpdateRuleFileType(i, value) {
if (!profilerDraft) return;
const r = profilerDraft.rules[i];
r.fileType = value;
const vis = profilerFileFields(value);
if (!vis.size) r.fileSize = null;
if (!vis.records) { r.recordLen = null; r.numRecords = null; }
if (r.content) {
const kind = profilerContentKindForFileType(value);
if (r.content.kind !== kind) {
r.content = kind === 'record'
? profilerEmptyRecordContent(r.content.mode)
: { mode: r.content.mode, kind: 'transparent', expected: '' };
}
}
profilerRenderEditor();
}
function profilerSetContent(i, mode) {
if (!profilerDraft) return;
const r = profilerDraft.rules[i];
@@ -7067,7 +7116,10 @@ function profilerSetContent(i, mode) {
} else if (r.content) {
r.content.mode = mode;
} else {
r.content = { mode: mode, kind: 'transparent', expected: '' };
const kind = profilerContentKindForFileType(r.fileType);
r.content = kind === 'record'
? profilerEmptyRecordContent(mode)
: { mode: mode, kind: 'transparent', expected: '' };
}
profilerRenderEditor();
}
@@ -7084,6 +7136,24 @@ function profilerUpdateRecord(i, ri, value) {
if (c && c.records) c.records[ri].data = value;
}
function profilerAddRecord(i) {
if (!profilerDraft) return;
const c = profilerDraft.rules[i].content;
if (!c || c.kind !== 'record') return;
const lastNum = c.records.length ? c.records[c.records.length - 1].num : 0;
c.records.push({ num: lastNum + 1, data: '' });
profilerRenderEditor();
}
function profilerDeleteRecord(i, ri) {
if (!profilerDraft) return;
const c = profilerDraft.rules[i].content;
if (!c || c.kind !== 'record') return;
if (c.records.length <= 1) return;
c.records.splice(ri, 1);
profilerRenderEditor();
}
function profilerSaveProfile() {
if (!profilerDraft) return;
profilerDraft.name = document.getElementById('profiler-name').value.trim();
@@ -7143,7 +7213,8 @@ async function profilerRunRule(r) {
if (!ok) res.status = 'fail';
res.checks.push({ label: 'fileType', expected: r.fileType, actual: sel.file_type, ok: ok });
}
if (r.fileSize !== null && r.fileSize !== undefined) {
const isRecord = profilerContentKindForFileType(r.fileType) === 'record';
if (!isRecord && r.fileSize !== null && r.fileSize !== undefined) {
const ok = sel.file_size === r.fileSize;
if (!ok) { res.status = 'fail'; sizeMismatch = true; }
res.checks.push({ label: 'fileSize', expected: r.fileSize, actual: sel.file_size, ok: ok });
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'otaman-v44';
const CACHE = 'otaman-v47';
const URLS = [
'index.html',
'help.html',
+27 -1
View File
@@ -21,7 +21,7 @@ function extractFunc(src, name) {
return src.slice(m.index, i + 1);
}
const FNS = ['profilerNormHex', 'profilerMatch', 'profilerMatchMin', 'profilerMaskPrefix4', 'profilerValidateProfile'];
const FNS = ['profilerNormHex', 'profilerMatch', 'profilerMatchMin', 'profilerMaskPrefix4', 'profilerFileFields', 'profilerContentKindForFileType', 'profilerEmptyRecordContent', 'profilerValidateProfile'];
let code = '';
for (const f of FNS) code += extractFunc(html, f) + '\n';
eval(code);
@@ -74,6 +74,32 @@ test('profilerMaskPrefix4 keeps first 4 bytes and masks the rest', () => {
assert.strictEqual(profilerMaskPrefix4('1234'), '1234');
});
test('profilerFileFields maps file type to applicable fields', () => {
assert.deepStrictEqual(profilerFileFields('transparent'), { size: true, records: false });
assert.deepStrictEqual(profilerFileFields('ber_tlv'), { size: true, records: false });
assert.deepStrictEqual(profilerFileFields('linear_fixed'), { size: false, records: true });
assert.deepStrictEqual(profilerFileFields('cyclic'), { size: false, records: true });
assert.deepStrictEqual(profilerFileFields('df'), { size: false, records: false });
assert.deepStrictEqual(profilerFileFields(null), { size: true, records: true });
assert.deepStrictEqual(profilerFileFields(''), { size: true, records: true });
});
test('profilerContentKindForFileType', () => {
assert.strictEqual(profilerContentKindForFileType('linear_fixed'), 'record');
assert.strictEqual(profilerContentKindForFileType('cyclic'), 'record');
assert.strictEqual(profilerContentKindForFileType('transparent'), 'transparent');
assert.strictEqual(profilerContentKindForFileType('ber_tlv'), 'transparent');
assert.strictEqual(profilerContentKindForFileType('df'), 'transparent');
});
test('profilerEmptyRecordContent seeds one empty record row', () => {
const c = profilerEmptyRecordContent('exact');
assert.strictEqual(c.kind, 'record');
assert.strictEqual(c.mode, 'exact');
assert.strictEqual(c.records.length, 1);
assert.deepStrictEqual(c.records[0], { num: 1, data: '' });
});
test('profile validation', () => {
assert.strictEqual(profilerValidateProfile(null), 'Not an object');
assert.strictEqual(profilerValidateProfile({ name: 'x' }), 'Missing rules array');