ui: clear stale RAM status on op change, remember the card preset

- ramOpChanged() now clears the previous execution status (result line,
  steps, explorer, progress) only when the Operation value actually
  changes, so switching Explore -> Install Package no longer shows the
  old 'Partial - ELF Modules: no data' until Execute; re-entering the
  RAM subtab still preserves the last result
- ramClearResults() also hides the progress caption
- the Card preset dropdown no longer resets to the placeholder after
  every executed operation (ramSaveCntr -> ramRender rebuilt it) or on
  subtab re-entry: ramRender keeps the current/remembered index,
  ramApplyCard and ramExecute remember the selection for the session,
  and cardsRemove keeps _ramCardIdx aligned via ramCardIdxAfterRemove
- tests: op-change clearing, selection preservation across rebuilds,
  pick/execute remembering, index bookkeeping; SW cache v126 -> v127.
This commit is contained in:
2026-09-12 23:20:26 +03:00
parent b49e6728b4
commit 2494e04984
3 changed files with 148 additions and 9 deletions
+22 -6
View File
@@ -4806,10 +4806,16 @@ async function pysimSendOta() {
// reuse /api/send-ota. Install Package sends the .cap hex to /api/ram-install // reuse /api/send-ota. Install Package sends the .cap hex to /api/ram-install
// which orchestrates INSTALL[for load] -> LOAD x N -> INSTALL[for install]. // which orchestrates INSTALL[for load] -> LOAD x N -> INSTALL[for install].
let _ramCardIdx = null;
let _ramOpLast = null;
function ramRender() { function ramRender() {
// populate the card preset selector from the in-memory cards[] array // populate the card preset selector from the in-memory cards[] array,
// keeping the current/remembered selection across rebuilds
const sel = document.getElementById('ram-card-sel'); const sel = document.getElementById('ram-card-sel');
if (!sel) return; if (!sel) return;
const prev = parseInt(sel.value, 10);
const keep = (!isNaN(prev) && cards[prev]) ? prev : _ramCardIdx;
sel.innerHTML = '<option value="" data-l10n="— Select card —">— Select card —</option>'; sel.innerHTML = '<option value="" data-l10n="— Select card —">— Select card —</option>';
cards.forEach((c, i) => { cards.forEach((c, i) => {
const opt = document.createElement('option'); const opt = document.createElement('option');
@@ -4817,23 +4823,23 @@ function ramRender() {
opt.textContent = c.name || ('Card ' + i); opt.textContent = c.name || ('Card ' + i);
sel.appendChild(opt); sel.appendChild(opt);
}); });
if (keep !== null && keep !== undefined && cards[keep]) sel.value = String(keep);
ramOpChanged(); ramOpChanged();
} }
function ramApplyCard(idx) { function ramApplyCard(idx) {
// copy the selected card preset into the SP form fields so that // copy the selected card preset into the SP form fields so that
// getRamSpParams() picks up the right SPI/KIc/KID/TAR/CNTR/keys // getRamSpParams() picks up the right SPI/KIc/KID/TAR/CNTR/keys
const i = parseInt(idx, 10);
if (!isNaN(i) && cards[i]) _ramCardIdx = i;
cardsApply(idx); cardsApply(idx);
} }
function ramOpChanged() { function ramOpChanged() {
const op = document.getElementById('ram-op').value; const op = document.getElementById('ram-op').value;
if (_ramOpLast !== null && op !== _ramOpLast) ramClearResults();
_ramOpLast = op;
document.getElementById('ram-install-params').classList.toggle('hidden', op !== 'install-cap'); document.getElementById('ram-install-params').classList.toggle('hidden', op !== 'install-cap');
if (op !== 'explore') {
_ramExplorerData = null;
document.getElementById('ram-explorer').classList.add('hidden');
document.getElementById('ram-explorer').innerHTML = '';
}
} }
function ramShowProgress(text) { function ramShowProgress(text) {
@@ -4848,6 +4854,7 @@ function ramHideProgress() {
function ramClearResults() { function ramClearResults() {
_ramExplorerData = null; _ramExplorerData = null;
ramHideProgress();
document.getElementById('ram-result').classList.add('hidden'); document.getElementById('ram-result').classList.add('hidden');
document.getElementById('ram-explorer').classList.add('hidden'); document.getElementById('ram-explorer').classList.add('hidden');
document.getElementById('ram-explorer').innerHTML = ''; document.getElementById('ram-explorer').innerHTML = '';
@@ -5384,6 +5391,8 @@ async function ramInstallCap(sp) {
async function ramExecute() { async function ramExecute() {
ramClearResults(); ramClearResults();
const cardIdx = parseInt(document.getElementById('ram-card-sel').value, 10);
if (!isNaN(cardIdx) && cards[cardIdx]) _ramCardIdx = cardIdx;
const op = document.getElementById('ram-op').value; const op = document.getElementById('ram-op').value;
const sp = getRamSpParams(); const sp = getRamSpParams();
if (!sp.kicKey || !sp.kidKey) { if (!sp.kicKey || !sp.kidKey) {
@@ -5589,7 +5598,14 @@ function cardsAdd() {
}); });
} }
function ramCardIdxAfterRemove(idx, removedIdx) {
if (idx === null || idx === undefined || idx < 0) return null;
if (idx === removedIdx) return null;
return idx > removedIdx ? idx - 1 : idx;
}
function cardsRemove(i) { function cardsRemove(i) {
_ramCardIdx = ramCardIdxAfterRemove(_ramCardIdx, i);
cards.splice(i, 1); cards.splice(i, 1);
cardsSave(); cardsSave();
cardsRender(); cardsRender();
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'otaman-v126'; const CACHE = 'otaman-v127';
const URLS = [ const URLS = [
'index.html', 'index.html',
'help.html', 'help.html',
+125 -2
View File
@@ -6,7 +6,7 @@ const path = require('node:path');
const html = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf8'); const html = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf8');
function extractFunc(src, name) { function extractFunc(src, name) {
const re = new RegExp('function\\s+' + name + '\\s*\\([^)]*\\)\\s*\\{'); const re = new RegExp('(?:async\\s+)?function\\s+' + name + '\\s*\\([^)]*\\)\\s*\\{');
const m = re.exec(src); const m = re.exec(src);
if (!m) throw new Error('function ' + name + ' not found'); if (!m) throw new Error('function ' + name + ' not found');
let i = m.index + m[0].length - 1; let i = m.index + m[0].length - 1;
@@ -22,7 +22,8 @@ function extractFunc(src, name) {
} }
// Extract chain builder functions and dependencies // 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',
'ramCardIdxAfterRemove', 'ramClearResults', 'ramHideProgress', 'ramOpChanged', 'ramRender', 'ramApplyCard', 'ramExecute'];
let code = ''; let code = '';
for (const f of FNS) { for (const f of FNS) {
code += extractFunc(html, f) + '\n'; code += extractFunc(html, f) + '\n';
@@ -32,6 +33,8 @@ if (m) code += m[0].replace(/^const /, 'var ') + '\n';
const lc = html.match(/const RAM_LIFECYCLE = \{[\s\S]*?\n\};/); const lc = html.match(/const RAM_LIFECYCLE = \{[\s\S]*?\n\};/);
if (lc) code += lc[0].replace(/^const /, 'var ') + '\n'; if (lc) code += lc[0].replace(/^const /, 'var ') + '\n';
eval(code); eval(code);
code += 'var _ramCardIdx = null;\nvar _ramOpLast = null;\nvar _ramExplorerData = null;\n';
eval(code);
const els = {}; const els = {};
const doc = { getElementById: (id) => { if (!els[id]) els[id] = {value:''}; return els[id]; } }; const doc = { getElementById: (id) => { if (!els[id]) els[id] = {value:''}; return els[id]; } };
@@ -197,3 +200,123 @@ test('ramFmtPrivileges uses the translated (none) placeholder', () => {
assert.strictEqual(ramFmtPrivileges('00'), 'XX(none)'); assert.strictEqual(ramFmtPrivileges('00'), 'XX(none)');
delete global.t; delete global.t;
}); });
function fakeClassList() {
const set = new Set();
return {
add: (...cs) => cs.forEach(c => set.add(c)),
remove: (...cs) => cs.forEach(c => set.delete(c)),
contains: c => set.has(c),
toggle: (c, on) => { if (on === undefined ? !set.has(c) : on) set.add(c); else set.delete(c); },
};
}
function fakeEl(id) {
return {
id,
value: '',
innerHTML: '',
textContent: '',
classList: fakeClassList(),
options: [],
appendChild(opt) { this.options.push(opt); },
};
}
function fakeRamDocument(ids) {
const els = {};
for (const id of ids) els[id] = fakeEl(id);
const sel = els['ram-card-sel'];
if (sel) {
Object.defineProperty(sel, 'innerHTML', {
get() { return this._html || ''; },
set(v) { this._html = v; this.value = ''; },
});
}
globalThis.document = {
getElementById: id => els[id] || null,
createElement: () => fakeEl('option'),
};
return els;
}
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;
els['ram-op'].value = 'explore';
ramOpChanged();
assert.ok(!els['ram-result'].classList.contains('hidden'));
els['ram-result'].classList.remove('hidden');
els['ram-steps'].classList.remove('hidden');
ramOpChanged();
assert.ok(!els['ram-result'].classList.contains('hidden'), 'same op must keep the result');
els['ram-op'].value = 'install-cap';
ramOpChanged();
assert.ok(els['ram-result'].classList.contains('hidden'));
assert.ok(els['ram-steps'].classList.contains('hidden'));
assert.ok(els['ram-explorer'].classList.contains('hidden'));
assert.ok(els['ram-progress'].classList.contains('hidden'));
assert.ok(!els['ram-install-params'].classList.contains('hidden'));
});
test('ramRender keeps the selected card preset across rebuilds', () => {
const els = fakeRamDocument(['ram-card-sel', 'ram-op', 'ram-install-params', 'ram-result', 'ram-explorer', 'ram-steps', 'ram-progress']);
globalThis.cards = [{ name: 'A' }, { name: 'B' }, { name: 'C' }];
_ramCardIdx = null;
_ramOpLast = 'explore';
els['ram-op'].value = 'explore';
ramRender();
assert.strictEqual(els['ram-card-sel'].value, '');
els['ram-card-sel'].value = '1';
ramRender();
assert.strictEqual(els['ram-card-sel'].value, '1');
els['ram-card-sel'].value = '';
_ramCardIdx = 2;
ramRender();
assert.strictEqual(els['ram-card-sel'].value, '2');
globalThis.cards = [{ name: 'A' }];
_ramCardIdx = 2;
ramRender();
assert.strictEqual(els['ram-card-sel'].value, '');
delete globalThis.cards;
});
test('ramApplyCard remembers a valid picked preset', () => {
globalThis.cards = [{ name: 'A' }, { name: 'B' }];
let applied = null;
globalThis.cardsApply = i => { applied = i; };
_ramCardIdx = null;
ramApplyCard('1');
assert.strictEqual(_ramCardIdx, 1);
assert.strictEqual(applied, '1');
ramApplyCard('');
assert.strictEqual(_ramCardIdx, 1, 'invalid pick must not forget the preset');
delete globalThis.cards;
delete globalThis.cardsApply;
});
test('ramExecute commits the dropdown selection before running', async () => {
const els = fakeRamDocument(['ram-card-sel', 'ram-op', 'ram-install-params', 'ram-result', 'ram-explorer', 'ram-steps', 'ram-progress']);
globalThis.cards = [{ name: 'A' }];
globalThis.getRamSpParams = () => ({ kicKey: '11', kidKey: '22' });
let explored = false;
globalThis.ramExplore = async () => { explored = true; };
globalThis.alert = () => {};
_ramCardIdx = null;
els['ram-card-sel'].value = '0';
els['ram-op'].value = 'explore';
await ramExecute();
assert.strictEqual(_ramCardIdx, 0);
assert.ok(explored);
delete globalThis.cards;
delete globalThis.getRamSpParams;
delete globalThis.ramExplore;
delete globalThis.alert;
});
test('ramCardIdxAfterRemove keeps the remembered index aligned', () => {
assert.strictEqual(ramCardIdxAfterRemove(2, 0), 1);
assert.strictEqual(ramCardIdxAfterRemove(0, 0), null);
assert.strictEqual(ramCardIdxAfterRemove(0, 2), 0);
assert.strictEqual(ramCardIdxAfterRemove(null, 1), null);
});