ui: gate controls by server/card state; move Custom files to Profiler

Availability model: server-down / server-up-no-card / card-equipped.
- header shows a symbolic indicator dot (red/yellow/green, tooltip)
- a 5s probe tracks /api/version+/api/status while not fully ready; the
  2s status poll takes over once connected
- controls declare data-needs='server' or 'card'; disabled + tooltip when
  the state does not satisfy them: equip (server), status/reset, file
  manager read/save/edit, raw APDU, profile-from-card, new snapshot,
  profiler list Check card, event Send, RAM install/explore delete,
  STK menu, Send STATUS, Verify vs pySim (server), pySim execute (server),
  PLI save/poll toggle (server)
- pysimConnect no longer conflates server and card: it records both and
  Connect stays usable without a card
- ramSendOta/ramInstallCap now use pysimFetch instead of raw fetch
  (relative /api URLs broke custom server URLs)

Custom files moved from the Card reader sub-tabs to the Profiler list
tabs (Profiles / Card snapshots / Custom files); help/README/AGENTS
updated. Tests: availability gating, indicator structure, moved tab.
SW cache v103 -> v104.
This commit is contained in:
2026-09-12 17:58:30 +03:00
parent f1ed6a2a48
commit 5b9e821d6d
9 changed files with 235 additions and 103 deletions
+24 -2
View File
@@ -21,8 +21,11 @@ function extractFunc(src, name) {
return src.slice(m.index, i + 1);
}
let code = 'var _pysimCardStateKey = null;\nvar _pysimCardSession = null;\n';
let code = 'var _pysimCardStateKey = null;\nvar _pysimCardSession = null;\n'
+ 'var _pysimServerAvailable = null;\nvar _pysimCardEquipped = false;\n';
code += extractFunc(html, 'pysimCardStateUpdate') + '\n';
code += extractFunc(html, 'pysimAvailabilityState') + '\n';
code += extractFunc(html, 'pysimControlDisabled') + '\n';
code += '\nglobalThis.esc = s => s;\n';
code += 'globalThis.t = s => s;\n';
eval(code);
@@ -32,9 +35,10 @@ function setup() {
const calls = { connected: [], resets: [], refreshStatus: [] };
_pysimCardStateKey = null;
_pysimCardSession = null;
globalThis.document = { getElementById: () => el };
globalThis.document = { getElementById: () => el, querySelectorAll: () => [] };
globalThis.pysimSetConnected = v => calls.connected.push(v);
globalThis.pysimResetCardData = refresh => calls.resets.push(refresh);
globalThis.pysimApplyAvailability = () => {};
return { el, calls };
}
@@ -98,3 +102,21 @@ test('payload without connected flag is ignored', () => {
pysimCardStateUpdate(null);
assert.deepStrictEqual(calls.connected, []);
});
test('availability state and control gating follow server/card state', () => {
_pysimServerAvailable = null;
assert.strictEqual(pysimAvailabilityState(), 'server-down');
assert.strictEqual(pysimControlDisabled('server', 'server-down'), true);
assert.strictEqual(pysimControlDisabled('card', 'server-down'), true);
_pysimServerAvailable = true;
_pysimCardEquipped = false;
assert.strictEqual(pysimAvailabilityState(), 'no-card');
assert.strictEqual(pysimControlDisabled('server', 'no-card'), false);
assert.strictEqual(pysimControlDisabled('card', 'no-card'), true);
_pysimCardEquipped = true;
assert.strictEqual(pysimAvailabilityState(), 'card');
assert.strictEqual(pysimControlDisabled('card', 'card'), false);
assert.strictEqual(pysimControlDisabled('server', 'card'), false);
});