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': 'При несовпадении',