diff --git a/README.md b/README.md index 9be73c4..2d17bbd 100644 --- a/README.md +++ b/README.md @@ -534,7 +534,7 @@ The list view has two tabs — **Profiles** and **Card snapshots**. A snapshot i - **New snapshot** scans the card; **Import snapshot** loads JSON. - Each snapshot row has **Open** (all captured data read-only, raw FCI with decoded FCI and contents; only the name is editable), **Export**, and **Delete**. - **Check card snapshot** on a profile row runs the profile rules against a snapshot picked from the list, without a card reader. Files whose contents were not captured are reported as unverifiable errors. -- **Compare snapshots** compares two snapshots offline exactly like a profile check: pick the *master* snapshot and the *snapshot to check*, optionally masking the first 4 bytes of EF.IMSI/EF.ICCID (on by default), and get the same report. Every file must match exactly (exact FCI, contents); files present only in the checked snapshot are reported as extra files. +- **Compare snapshots** compares two snapshots offline exactly like a profile check: pick the *master* snapshot and the *snapshot to check*, optionally masking the first 4 bytes of EF.IMSI/EF.ICCID (on by default), and get the same report. Every file must match exactly (exact FCI, contents); files present only in the checked snapshot are reported as extra files. In the comparison report the mismatch fields and FCI comparison columns are labeled with the master/checked snapshot names instead of expected/actual. --- diff --git a/README_RUS.md b/README_RUS.md index 4a93797..cc7642a 100644 --- a/README_RUS.md +++ b/README_RUS.md @@ -508,7 +508,7 @@ Delivery PoR (SPI2 `01`) проще — карта возвращает PoR на - **Новый снимок** сканирует карту; **Импорт снимка** загружает JSON. - В строке снимка: **Открыть** (все данные только для чтения, сырой FCI с декодированным и содержимое; редактируется только имя), **Экспорт**, **Удалить**. - **Проверить снимок карты** в строке профиля выполняет правила профиля на выбранном снимке без картридера. Файлы без захваченного содержимого помечаются как непроверяемые ошибки. -- **Сравнить снимки** сравнивает два снимка offline так же, как проверка профиля: выберите *эталонный* снимок и *снимок для проверки*, при необходимости включите маску первых 4 байт EF.IMSI/EF.ICCID (включена по умолчанию). Всё должно совпадать точно (FCI, содержимое); файлы только в проверяемом снимке помечаются как лишние. +- **Сравнить снимки** сравнивает два снимка offline так же, как проверка профиля: выберите *эталонный* снимок и *снимок для проверки*, при необходимости включите маску первых 4 байт EF.IMSI/EF.ICCID (включена по умолчанию). Всё должно совпадать точно (FCI, содержимое); файлы только в проверяемом снимке помечаются как лишние. В отчёте поля расхождений и колонки сравнения FCI подписаны именами эталонного и проверяемого снимков вместо expected/actual. --- diff --git a/frontend/help-ru.html b/frontend/help-ru.html index 4fc65ec..e12a58c 100644 --- a/frontend/help-ru.html +++ b/frontend/help-ru.html @@ -400,7 +400,7 @@
  • Импорт снимка — загружает снимок из JSON-файла.
  • В каждой строке снимка — Открыть, Экспорт и Удалить. Открыть показывает все захваченные данные только для чтения (сырой FCI с декодированным FCI, содержимое); редактируется только имя снимка.
  • Проверить снимок карты в строке профиля выполняет правила профиля на выбранном из списка снимке, без картридера. Отчёт такой же, как при проверке карты; файлы, содержимое которых не было захвачено при сканировании, помечаются как непроверяемые ошибки.
  • -
  • Сравнить снимки сравнивает два снимка без картридера так же, как проверка профиля: выберите эталонный снимок и снимок для проверки, при необходимости включите маску первых 4 байт EF.IMSI/EF.ICCID (включена по умолчанию) и получите такой же отчёт. Файлы, которые есть только в проверяемом снимке, помечаются как лишние. «К списку» возвращает на вкладку «Снимки карт».
  • +
  • Сравнить снимки сравнивает два снимка без картридера так же, как проверка профиля: выберите эталонный снимок и снимок для проверки, при необходимости включите маску первых 4 байт EF.IMSI/EF.ICCID (включена по умолчанию) и получите такой же отчёт; в этом отчёте поля расхождений и колонки сравнения FCI подписаны именами эталонного и проверяемого снимков вместо expected/actual. Файлы, которые есть только в проверяемом снимке, помечаются как лишние. «К списку» возвращает на вкладку «Снимки карт».
  • diff --git a/frontend/help.html b/frontend/help.html index a31c85d..0d28186 100644 --- a/frontend/help.html +++ b/frontend/help.html @@ -400,7 +400,7 @@
  • Import snapshot — loads a snapshot from a JSON file.
  • Each snapshot row has Open, Export, and Delete. Open shows all captured data read-only (raw FCI with the decoded FCI, contents); only the snapshot name is editable.
  • Check card snapshot on a profile row runs the profile rules against a snapshot you pick from the list, without a card reader. The report is the same as a live check; files whose contents were not captured during the scan are reported as unverifiable errors.
  • -
  • Compare snapshots compares two snapshots offline, exactly like a profile check: pick the master snapshot and the snapshot to check, optionally masking the first 4 bytes of EF.IMSI/EF.ICCID (on by default), and get the same pass/fail report. Files present only in the checked snapshot are reported as extra files. Back to list returns to the Card snapshots tab.
  • +
  • Compare snapshots compares two snapshots offline, exactly like a profile check: pick the master snapshot and the snapshot to check, optionally masking the first 4 bytes of EF.IMSI/EF.ICCID (on by default), and get the same pass/fail report; in that report the mismatch fields and the FCI comparison columns are labeled with the master and checked snapshot names instead of expected/actual. Files present only in the checked snapshot are reported as extra files. Back to list returns to the Card snapshots tab.
  • diff --git a/frontend/index.html b/frontend/index.html index f25ae6e..dbe794d 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -7089,6 +7089,7 @@ let profilerView = 'list'; let profilerEditId = null; let profilerDraft = null; let profilerResults = null; +let profilerResultsLabels = null; let profilerMismatchOnly = false; let snapshots = []; let snapshotViewId = null; @@ -8113,7 +8114,7 @@ async function profilerCheck(i) { await profilerRunProfile(p, null, p.name); } -async function profilerRunProfile(p, source, title, extraResults) { +async function profilerRunProfile(p, source, title, extraResults, labels) { profilerSetView('results'); document.getElementById('profiler-results-title').textContent = title; const prog = document.getElementById('profiler-progress'); @@ -8128,6 +8129,7 @@ async function profilerRunProfile(p, source, title, extraResults) { if (extraResults && extraResults.length) results.push(...extraResults); prog.textContent = ''; profilerResults = results; + profilerResultsLabels = labels || null; profilerRenderResultsView(); } @@ -8238,7 +8240,8 @@ async function snapshotCompareRun() { }); document.getElementById('snapshot-compare-modal').classList.add('hidden'); const pseudo = { name: master.name + ' → ' + check.name, rules: profilerRulesFromSnapshot(master, maskFids) }; - await profilerRunProfile(pseudo, profilerSnapshotSource(check), pseudo.name, profilerExtraFileResults(master, check)); + await profilerRunProfile(pseudo, profilerSnapshotSource(check), pseudo.name, profilerExtraFileResults(master, check), + { expected: master.name, actual: check.name }); } function profilerToggleMismatchOnly(checked) { @@ -8263,7 +8266,7 @@ function profilerRenderResultsView() { ' · ' + errors + ' ' + t('errors') + ''; const shown = profilerVisibleResults(profilerResults, profilerMismatchOnly); document.getElementById('profiler-report').innerHTML = shown.length - ? profilerRenderReport(shown) + ? profilerRenderReport(shown, profilerResultsLabels) : '
    ' + esc(t('No mismatches')) + '
    '; } @@ -8684,7 +8687,9 @@ function fcpDecode(hex) { return { ok: !error && items.length > 0, template, items, error }; } -function fcpDiffHtml(expectedHex, actualHex) { +function fcpDiffHtml(expectedHex, actualHex, labels) { + const expLabel = (labels && labels.expected) ? labels.expected : t('expected'); + const actLabel = (labels && labels.actual) ? labels.actual : t('actual'); const e = fcpDecode(expectedHex); const a = fcpDecode(actualHex); const eBad = !!e.error, aBad = !!a.error; @@ -8700,8 +8705,8 @@ function fcpDiffHtml(expectedHex, actualHex) { html += ''; html += '' + '' + - '' + - ''; + '' + + ''; for (const k of keys) { const ev = eMap[k], av = aMap[k]; const ed = ev ? (ev.decoded === null ? '' : (ev.decoded || ev.value)) : '—'; @@ -8744,7 +8749,10 @@ function profilerFciInput(i, value) { profilerUpdateFciPreview(i, value); } -function profilerRenderReport(results) { +function profilerRenderReport(results, labels) { + const expLabel = (labels && labels.expected) ? labels.expected : t('expected'); + const actLabel = (labels && labels.actual) ? labels.actual : t('actual'); + const labelCls = 'text-xs text-gray-500 dark:text-slate-400 text-right shrink-0 ' + (labels ? 'w-40 break-all' : 'w-24'); let html = ''; for (const r of results) { const color = r.status === 'pass' ? 'text-emerald-600' : (r.status === 'fail' ? 'text-red-600' : 'text-yellow-600'); @@ -8768,15 +8776,15 @@ function profilerRenderReport(results) { html += '
    '; html += '
    ' + esc(c.label) + '
    '; html += '
    ' + - '' + esc(t('expected')) + '' + + '' + esc(expLabel) + '' + '
    '; html += '
    ' + - '' + esc(t('actual')) + '' + + '' + esc(actLabel) + '' + '
    '; - if (c.label === 'fci') html += fcpDiffHtml(String(c.expected), String(c.actual)); + if (c.label === 'fci') html += fcpDiffHtml(String(c.expected), String(c.actual), labels); html += '
    '; } else { - html += '
    ' + esc(c.label) + ': ' + esc(t('expected')) + ' ' + esc(String(c.expected)) + ', ' + esc(t('actual')) + ' ' + esc(String(c.actual)) + '
    '; + html += '
    ' + esc(c.label) + ': ' + esc(expLabel) + ' ' + esc(String(c.expected)) + ', ' + esc(actLabel) + ' ' + esc(String(c.actual)) + '
    '; } } const recFailed = r.recordsMismatch || (r.checks || []).some(c => /^content\.rec\d+$/.test(c.label) && !c.ok); diff --git a/frontend/sw.js b/frontend/sw.js index 1ab3f96..a158823 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'otaman-v101'; +const CACHE = 'otaman-v102'; const URLS = [ 'index.html', 'help.html', diff --git a/frontend/tests/profiler.test.js b/frontend/tests/profiler.test.js index c0d7fde..96e12c1 100644 --- a/frontend/tests/profiler.test.js +++ b/frontend/tests/profiler.test.js @@ -584,7 +584,7 @@ test('profilerRenderReport renders raw-data mismatches as aligned readonly field assert.ok(html.includes('font-mono')); assert.ok(html.includes('value="621082024021"')); assert.ok(html.includes('value="621082024022"')); - assert.ok(html.includes('w-24 text-right')); + assert.ok(html.includes('text-right shrink-0 w-24')); // non-raw check stays inline assert.ok(html.includes('content.records: expected')); assert.ok(!fciBlock.includes(': expected')); @@ -1227,3 +1227,42 @@ test('profilerSnapshotCountLabel appends the scan time when available', () => { assert.strictEqual(empty, '0 files'); delete global.t; }); + +test('profilerRenderReport labels mismatches with custom names', () => { + global.t = s => s; + global.pysimCustomFiles = []; + const html = profilerRenderReport([{ + path: 'MF/6F3A', name: 'EF.ADN', status: 'fail', + checks: [ + { label: 'content', expected: 'AABB', actual: 'CCDD', ok: false }, + { label: 'fileSize', expected: 4, actual: 9, ok: false }, + ], + }], { expected: 'Master snap', actual: 'Check snap' }); + assert.ok(html.includes('Master snap'), html); + assert.ok(html.includes('Check snap'), html); + assert.ok(!html.includes('>expected<'), html); + assert.ok(!html.includes('>actual<'), html); + delete global.t; + delete global.pysimCustomFiles; +}); + +test('profilerRenderReport keeps expected/actual without labels', () => { + global.t = s => s; + global.pysimCustomFiles = []; + const html = profilerRenderReport([{ + path: 'MF/6F3A', status: 'fail', + checks: [{ label: 'fileSize', expected: 4, actual: 9, ok: false }], + }]); + assert.ok(html.includes(' expected '), html); + assert.ok(html.includes(' actual '), html); + delete global.t; + delete global.pysimCustomFiles; +}); + +test('fcpDiffHtml headers use custom labels', () => { + global.t = s => s; + const diff = fcpDiffHtml(FCP_TRANSPARENT, '62128002000A8202412183026F078A0105880110', { expected: 'Master', actual: 'Candidate' }); + assert.ok(diff.includes('>Master<'), diff); + assert.ok(diff.includes('>Candidate<'), diff); + delete global.t; +});
    ' + esc(t('Parameter')) + '' + esc(t('expected')) + '' + esc(t('actual')) + '
    ' + esc(expLabel) + '' + esc(actLabel) + '