fix: report the real PoR/remote-SW result of every RAM install step (v3.6.2)

The RAM installer read `por['decoded']['response_status']`, but the compact
response decoder's `decoded` only carries {number_of_commands,
last_status_word, last_response_data} - so the suffix was always empty and
every step showed the useless `por_error_`, even when the PoR was por_ok.
The PoR verdict is the top-level `response_status`.

With that fixed, the decoded details also show that the remote command's own
status word was the real verdict all along: a captured live install returned
por_ok with `last_status_word` 6700/6F00 (the card rejected every RAM APDU)
while the installer reported success.

- new `_ram_step_result()`/`_por_remote_sw()`/`_ram_remote_sw_ok()`: a step
  fails on a non-por_ok PoR, on a remote SW outside the success set (9000,
  61xx more data, 62xx/63xx warnings, CAFE GP "more data"), or on an
  undecodable PoR (a 9000 transport SW with no PoR at all is `no_por`, not a
  failure).  A decoded 61xx is explicitly success, per GP/ISO.
- step records gain por_sw/por_type/por_cntr/por_data/por_raw and
  `por_error`; the response carries `failed_step` and a detailed error such
  as `LOAD (1/9): remote SW 6700`; the log line now prints
  `status=por_ok remote_sw=6700`.
- PWA: new pure `ramStepLine()` renders e.g.
  `❌ Шаг 2: LOAD (1/9) — PoR ok · remote SW 6700 (Wrong length in Lc) · 274 B / 3 SMS`
  (SW meaning via the existing lookupSw decoder) and the transport SW only
  when it is not 9000.
- tests: tests/test_ota_helpers.py RamPorStepTest (success set incl. 61xx,
  the captured 6700 failure, no-PoR/undecodable, expanded responses);
  ram.test.js ramStepLine cases.
- docs/api.md RAM install step fields + failure semantics; AGENTS updated.

603 frontend / 487 Python green; version 3.6.2; sw simple-v275.
This commit is contained in:
2026-09-27 23:21:56 +03:00
parent b92a47cd44
commit 36d2f71bc7
7 changed files with 215 additions and 27 deletions
+35 -7
View File
@@ -1665,7 +1665,7 @@
// ===== Version =====
// Single source of truth for the PWA version: shown in the header and used
// by the server version check in pysimConnect().
const SIMPLE_VERSION = '3.6.1';
const SIMPLE_VERSION = '3.6.2';
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
// ===== Tab switching =====
@@ -8859,6 +8859,33 @@ async function ramDeleteFromExplorer(aid, withCascade) {
await ramExplore(sp);
}
// One RAM install step line: the PoR verdict plus the remote command's
// status word (9000/61xx/62xx/63xx/CAFE are success; anything else is the
// card's error) - the details that used to be lost behind a bare
// "por_error_" label. The envelope/GET RESPONSE SW is only shown when it is
// not a plain 9000.
function ramStepLine(s, idx) {
const ok = !s.por_error && (s.por_status === 'por_ok' || s.por_status === 'no_por');
let line = (ok ? '\u2705' : '\u274c') + ' ' + t('Step') + ' ' + (idx + 1) + ': ' +
s.name + ' \u2014 ';
if (s.por_status === 'por_ok') line += t('PoR ok');
else if (s.por_status === 'no_por') line += t('no PoR');
else if (s.por_status && s.por_status.indexOf('por_error') === 0) {
const legacy = s.por_status.replace(/^por_error_?/, '');
line += t('PoR error') + (legacy ? ' ' + legacy : '');
} else if (s.por_status) line += t('PoR error') + ' ' + s.por_status;
else line += t('PoR unknown');
if (s.por_sw) {
const name = lookupSw(s.por_sw.substr(0, 2), s.por_sw.substr(2, 2), 'gp');
line += ' \u00b7 ' + t('remote SW') + ' ' + s.por_sw + (name ? ' (' + name + ')' : '');
}
if (s.sw && s.sw !== '9000') line += ' \u00b7 ' + t('transport SW') + ' ' + s.sw;
if (s.bytes && s.segments) {
line += ' \u00b7 ' + s.bytes + ' ' + t('bytes') + ' / ' + s.segments + ' SMS';
}
return line;
}
async function ramInstallCap(sp) {
const fileInput = document.getElementById('ram-cap-file');
const file = fileInput.files[0];
@@ -8894,12 +8921,7 @@ async function ramInstallCap(sp) {
stepsEl.classList.remove('hidden');
let txt = '';
(data.steps || []).forEach((s, idx) => {
const mark = s.por_status === 'por_ok' ? '✅' : '❌';
let line = mark + ' ' + t('Step') + ' ' + (idx + 1) + ': ' + s.name + ' — ' + s.por_status + ' (SW ' + s.sw + ')';
if (s.bytes && s.segments) line += ' · ' + s.bytes + ' ' + t('bytes') + ' / ' + s.segments + ' SMS';
txt += line + '\n';
});
(data.steps || []).forEach((s, idx) => { txt += ramStepLine(s, idx) + '\n'; });
stepsEl.textContent = txt;
if (data.success) {
@@ -16787,6 +16809,12 @@ const LANG_RU = {
'default: 9000 (91?? when polling)': 'по умолчанию: 9000 (91?? при опросе)',
'Expected data (hex, optional)': 'Ожидаемые данные (hex, опционально)',
'PoR check': 'Проверка PoR',
'PoR ok': 'PoR: успешно',
'no PoR': 'PoR: нет',
'PoR error': 'Ошибка PoR',
'PoR unknown': 'PoR: неизвестно',
'remote SW': 'SW команды',
'transport SW': 'транспортный SW',
'Any': 'Любой',
'No PoR expected': 'PoR не ожидается',
'On mismatch': 'При несовпадении',
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'simple-v274';
const CACHE = 'simple-v275';
const URLS = [
'index.html',
'help.html',
+20 -1
View File
@@ -22,7 +22,7 @@ function extractFunc(src, name) {
}
// Extract chain builder functions and dependencies
const FNS = ['berLenStr', 'buildApdu', 'escHtml', 'esc', 'chainInit', 'chainRamBuildRowHex', 'ramFmtLifecycle', 'ramFmtPrivileges', 'ramRenderExploreHtml',
const FNS = ['berLenStr', 'buildApdu', 'escHtml', 'esc', 'chainInit', 'chainRamBuildRowHex', 'ramFmtLifecycle', 'ramFmtPrivileges', 'ramRenderExploreHtml', 'ramStepLine',
'ramCardIdxAfterRemove', 'ramClearResults', 'ramHideProgress', 'ramOpChanged', 'ramRender', 'ramApplyCard', 'ramExecute',
'jcAidNorm', 'jcAidName', 'jcAidSuffix', 'jcAidHtml'];
let code = '';
@@ -259,6 +259,25 @@ function fakeRamDocument(ids) {
return els;
}
test('ramStepLine shows the PoR verdict and the remote status word', () => {
globalThis.t = s => s;
globalThis.lookupSw = (a, b) => (a + b === '6700' ? 'Wrong length in Lc' : '');
const okLine = ramStepLine({ name: 'INSTALL [for load]', por_status: 'por_ok',
por_sw: '9000', sw: '9000', bytes: 50, segments: 1 }, 0);
assert.ok(okLine.startsWith('\u2705'), okLine);
assert.ok(okLine.includes('PoR ok'), okLine);
assert.ok(okLine.includes('remote SW 9000'), okLine);
assert.ok(okLine.includes('50 bytes / 1 SMS'), okLine);
const badLine = ramStepLine({ name: 'LOAD (1/9)', por_status: 'por_ok', por_sw: '6700',
por_error: 'remote SW 6700', sw: '9000', bytes: 274, segments: 3 }, 1);
assert.ok(badLine.startsWith('\u274c'), badLine);
assert.ok(badLine.includes('remote SW 6700 (Wrong length in Lc)'), badLine);
const porLine = ramStepLine({ name: 'LOAD', por_status: 'por_error_cntr_low' }, 2);
assert.ok(porLine.includes('PoR error cntr_low'), porLine);
const noPor = ramStepLine({ name: 'LOAD', por_status: 'no_por' }, 3);
assert.ok(noPor.startsWith('\u2705') && noPor.includes('no PoR'), noPor);
});
test('ramOpChanged clears the executed status only on a real op change', () => {
const els = fakeRamDocument(['ram-op', 'ram-install-params', 'ram-result', 'ram-explorer', 'ram-steps', 'ram-progress']);
_ramOpLast = null;