release: v3.9.0 - the operation start buttons wait for a preset and a keyset

Gating: the SCP80 "Send to Card", the RAM Execute and the TAR-probe Execute
buttons carry `data-needs-keyset`; pysimApplyAvailability() disables them
(with the hint "Select a card preset and its keyset" when the card itself is
fine) unless the view's preset is selected and the form's keyset number is one
of its keysets - `spKeysetStartReady`, the same rule the click-time guards
enforce, which stay as the last line of defence.  spKeysetSync() refreshes the
gate on every preset/keyset change, and the KIc/KID index selects now call
spKeysetSync() so the keyset selector follows a manual index change too.

Version 3.9.0: server VERSION, pyproject.toml and the PWA header; sw cache
simple-v346; the workspace AGENTS gains its v3.9.0 history entry.

Tests: indicator.test.js drives the gate (no preset -> disabled with the hint,
an undefined keyset -> disabled, a matching keyset -> enabled) and guards the
three buttons' attribute; the cards/keysets/ram sandboxes stub
pysimApplyAvailability.  Help EN/RU notes the disabled start buttons.
This commit is contained in:
2026-10-01 22:03:36 +03:00
parent f261695b86
commit 0cccd62230
11 changed files with 91 additions and 15 deletions
+2 -1
View File
@@ -33,7 +33,8 @@ for (const fn of ['cardsTarValue', 'cardsApplyFields', 'cardsApply',
]) {
code += extractFunc(html, fn) + '\n';
}
code = 'var _cardsKeysetCount = 0;\nglobalThis.spPresetWarningRender = function() {};\n' + code;
code = 'var _cardsKeysetCount = 0;\nglobalThis.spPresetWarningRender = function() {};\n'
+ 'globalThis.pysimApplyAvailability = function() {};\n' + code;
code = 'var _genSpBuildStub = function() {};\n' + code;
eval(code);
globalThis._genSpBuild = () => {};
+2 -1
View File
@@ -37,7 +37,8 @@ for (const fn of ['cardsTarValue', 'cardsFormValues', 'cardsClearForm', 'cardsEd
'ramKeysetChanged', 'ramPresetIdx', 'tarPresetIdx', 'spKeysetGuard', 'ramKeysetGuard']) {
code += extractFunc(html, fn, ASYNC_FNS.indexOf(fn) >= 0) + '\n';
}
code = 'var _cardsKeysetCount = 0;\nglobalThis.spPresetWarningRender = function() {};\n' + code;
code = 'var _cardsKeysetCount = 0;\nglobalThis.spPresetWarningRender = function() {};\n'
+ 'globalThis.pysimApplyAvailability = function() {};\n' + code;
eval(code);
const CARD_IDS = ['cards-name','cards-iccid','cards-adm',
+48
View File
@@ -34,6 +34,14 @@ code += extractFunc(html, 'pysimUpdatePresetIndicators') + '\n';
code += extractFunc(html, 'cardsScp80Complete') + '\n';
code += extractFunc(html, 'cardsScp81Complete') + '\n';
code += extractFunc(html, 'cardsMatchedPreset') + '\n';
code += extractFunc(html, 'spKeysetStartReady') + '\n';
code += extractFunc(html, 'spPresetIdx') + '\n';
code += extractFunc(html, 'ramPresetIdx') + '\n';
code += extractFunc(html, 'tarPresetIdx') + '\n';
code += extractFunc(html, 'spKeysetCheck') + '\n';
code += extractFunc(html, 'spKeysetFor') + '\n';
code += extractFunc(html, 'spKeysetList') + '\n';
code += extractFunc(html, 'spKeysetKvnOf') + '\n';
code += 'globalThis.t = s => s;\n';
code += 'globalThis.testRunActive = () => false;\n';
eval(code);
@@ -287,3 +295,43 @@ test('card action labels carry the equipped ICCID', () => {
assert.strictEqual(plain.textContent, '');
assert.strictEqual(colon.textContent, '');
});
test('the start buttons wait for a preset and a keyset defined in it', () => {
const run = (presetIdx, kic, kid) => {
const el = fakeEl();
el.setAttribute('data-needs', 'card');
el.setAttribute('data-needs-keyset', 'sp');
const els = {
'sp-card-sel': { value: presetIdx },
'sp-kic-hex': { value: kic },
'sp-kid-hex': { value: kid },
};
globalThis.document = { querySelectorAll: () => [el], getElementById: id => els[id] || null };
globalThis.cards = [{ name: 'EP', keysets: [
{ kic: '15', kid: '15', kicKey: 'AA', kidKey: 'BB', cntr: '0000000001' }] }];
_pysimServerAvailable = true;
_pysimCardEquipped = true;
pysimApplyAvailability();
return el;
};
// no preset selected -> disabled with the hint
let el = run('', '15', '15');
assert.strictEqual(el.disabled, true);
assert.strictEqual(el.attrs.title, 'Select a card preset and its keyset');
// a keyset number the preset does not define -> disabled
el = run('0', '35', '35');
assert.strictEqual(el.disabled, true);
// a keyset of the preset -> enabled, no tooltip
el = run('0', '15', '15');
assert.strictEqual(el.disabled, false);
assert.strictEqual(el.attrs.title, undefined);
});
test('the SCP80/RAM/TAR start buttons carry the keyset gate', () => {
const sp = /<button[^>]*id="sp-send-btn"[^>]*>/.exec(html)[0];
assert.match(sp, /data-needs-keyset="sp"/);
const ram = /<button[^>]*onclick="ramExecute\(\)"[^>]*>/.exec(html)[0];
assert.match(ram, /data-needs-keyset="ram"/);
const tar = /<button[^>]*onclick="tarExecute\(\)"[^>]*>/.exec(html)[0];
assert.match(tar, /data-needs-keyset="tar"/);
});
+1
View File
@@ -28,6 +28,7 @@ for (const fn of ['spKeysetKvnOf', 'spKeysetList', 'spKeysetFor', 'spKeysetCheck
code += extractFunc(html, fn) + '\n';
}
code += 'function t(s){return s;}\n';
code += 'globalThis.pysimApplyAvailability = () => {};\n';
eval(code);
const PRESET = {
+1
View File
@@ -50,6 +50,7 @@ if (an) code += an[0].replace(/^const /, 'var ') + '\n';
const ar = html.match(/const JC_AID_RIDS = \{[\s\S]*?\n\};/);
if (ar) code += ar[0].replace(/^const /, 'var ') + '\n';
globalThis.spPresetWarningRender = () => {};
globalThis.pysimApplyAvailability = () => {};
eval(code);
code += 'var _ramCardIdx = null;\nvar _ramOpLast = null;\nvar _ramExplorerData = null;\n';
eval(code);