The explorer's card-resources block now names the card's ETSI release, using
the same mapping as the CAP analysis (capmem.ETSI_TOOLKIT_RELEASE: uicc.toolkit
version -> REL-N, basis in brackets; the live card's 1.12 -> REL-15/16/17).
The table is mirrored in the PWA (drift-guarded by tests/test_cap_memory.py,
like JC_FRAMEWORK_SDK); the version comes from the expanded registry's CE tag
and degrades to the version hint when the listing has none; SIM-only cards (no
uicc.toolkit) get no label, the same rule as SIM-only CAPs.
- cardEtsiRelease(elfs) (pure) + UICC_TOOLKIT_AID / ETSI_TOOLKIT_RELEASE in the
PWA; rendered after the free-memory lines with a tooltip naming the basis.
- Tests: helper vectors (1.12/1.11/1.5, unknown version, versionless,
SIM-only), the rendering guard, and the table/AID drift guard.
- Help EN/RU; sw cache simple-v354.
The TCA loader's Explore traces settle the format question: it sends the same
P2=00 listing queries wrapped and unchained (`AA0A2208 80F24000024F0000`,
pages `...01024F0000`) - no GET RESPONSE anywhere - while the live card needs
the chain and answers 6700 to the wrapped unchained form; and the live ISD
refuses the compact P2=02 registry query (6700) while serving it wrapped
(`AA11220F 80F2400209 4F005C054F9F70C5CC00` -> 6310 + entries with CE
versions). Both the listing and the registry queries now follow the
operation's command format and fall back across formats.
- ramFormatIsExpanded(fmt) + ramFmtLadder(runFmt, working) (pure): the run's
format first, then expanded, expanded-ae and compact as the last resort; a
format that served data is tried first and never repeated.
- ramGetStatusApdu(p1, p2, fmt): the chain exists in compact only (TS 102 226
5.2.1.1) - an expanded run sends the bare query.
- paginate: the P2=00 listing follows the run's format and pins it per send,
so a GET RESPONSE can never ride inside an expanded packet.
- expandedPass: each registry form goes through the format ladder; the chained
61xx retry is compact-only (it was guarded for 'expanded' but not
'expanded-ae'); the serving format is remembered for the remaining listings
and named in the report ([AA]/[AE 80]); ramExpandedGroups carries it.
- Tests: the helper + ladder vectors, the unchained expanded listing builder
(both codings), the fallback/chain source guards, the report marker.
- Help EN/RU, the repo AGENTS.md; sw cache simple-v353.
Live findings that drove this: the "card counter 0000000000" the low-counter
notice showed is not the card's counter at all (the PoR's CNTR is a copy of
the command's counter, TS 102 225 5.2 table 3, and a rejection leaves it
zeroed); and an earlier "no card accepts the expanded format" observation was
a hand-made template whose declared length did not match its body - the card
validates the lengths and answers with a Bad format TLV.
- _cntr_low_fields(status) reports the verdict as a flag; card_cntr/
suggested_cntr and the UI's "(card counter ...)" text are gone - the value
is not observable. The notice offers a bounded Sync counter probe instead.
- POST /api/counter-probe: a doubling counter ladder (start+1, +2, +4, ...)
with a read-only command until the card accepts one, then accepted+1 is
persisted (monotonic, source counter-probe). Rejected attempts change
nothing on the card; the search stops at a ceiling (default FFFFFFFF, far
below the 40-bit maximum where the counter blocks) and after max_attempts
(default 40, clamped 1-64); a non-cntr_low failure stops it at once.
- _parse_response_scripting decodes the Bad format TLV (90, TS 101 220 table
7.20; error types per TS 102 226 table 5.12) and returns a dict; the PoR
carries bad_format/bad_format_name and _ram_step_result fails the step with
"bad format 02 (wrong length)" instead of falling through to the compact
decoder (which read the AB tag as 171 commands).
- _wrap_expanded_apdu(apdu, form) emits the definite (AA) or the indefinite
(AE 80 ... 00 00) scripting template; the RAM command formats are compact/
expanded/expanded-ae and _ram_detect_format probes compact, then both
expanded codings (the expanded probes use Le='00' and no chained GET
RESPONSE, TS 102 226 5.2.1.1). The RAM form gains a Format selector.
- Live-verified: the verdict carries no value; the probe walked
0x1000001/0x1000003 (low) then 0x1000007 (accepted, stored 0x1000008); the
ISD executes both expanded codings (R-APDU 08A0000000030000000F80, SW 9000)
and a length-mismatched template answers bad_format 02 - decoded now.
- Tests: the ladder, the probe walk + persistence, params validation, both
template forms, the Bad-format TLV (definite and plain tag styles), the
live ISD response through _decode_por, the step-result failure, and the PWA
guards; help EN/RU, docs/api.md, the repo AGENTS.md; sw cache simple-v352.
Live finding (Network rejection events): the card asks the terminal for the
current date and time (PROVIDE LOCAL INFORMATION, qualifier 03) and our TR
answered "performed successfully" with no data. TS 102 223 6.4.15 says the
terminal shall return the current date and time, and the 6.8.0 TR structure
requires the Date-Time and Time zone object (8.39) for that qualifier.
- _pli_data_hex(qual): the TR Config dictionary entry wins; when it is empty
the date/time qualifier is answered from the host clock via _encode_scts
(the SMS timestamp builder, TP-SCTS coding).
- _build_tr and _DefaultProactiveHandler put the command-specific data objects
after the Result TLV per the 6.8.0 order (the POLL INTERVAL Duration echo
too; real terminal responses pin the order: 8103010300 02028281 030100
0402011E).
- _decode_tr decodes qualifier 03 with the same 8.39 coding (swapped
semi-octet BCD digits, time-zone sign bit 0x08, 'FF' = unknown); it read
plain decimal bytes and a 0x80 sign before.
- The PWA PLI dictionary codec for the date/time entry writes the same coding
(it wrote plain hex bytes and a 0x80 sign), and the TR Config panel notes
the empty-entry default. sw cache simple-v351.
Tests: builder vectors (east/west/unknown zone) + dictionary override + the
empty non-date/time qualifier + the object order (PLI and POLL INTERVAL) + the
handler path + the TR decode + a frontend codec round-trip test.
The six forms that still missed conditional/optional objects now carry them:
- Location status (0x03): Location Information (MCC/MNC + LAC + Cell ID) -
included only for Normal service, as TS 102 223 8.19 requires.
- Channel status (0x0A): Bearer Description (B5, type + parameters) and
Other address (BE, IPv4 dotted / IPv6 hex / the empty "dynamic address"
form); new eventIpBytes() helper.
- Call disconnected (0x02): IMS call disconnection cause (D5, SIP/Q.850 +
2-byte cause).
- Network rejection (0x12): Extended information (F2, CAG ID / NID / RedCap).
- CAG cell selection (0x1E): CAG Human-readable network name list (D7,
80-tagged names).
- Slices status change (0x1F): Allowed slices with S-NSSAI mapping (F7),
Allowed slices (F8), Rejected slices with mapping (D7) and Rejected slices
(B1) beside the served list.
Every implemented event form now carries all the objects of its spec table.
Tests: the new build vectors. Help EN/RU and both AGENTS files; sw
simple-v350.
Group 1 (TS 102 223): Data available (Channel status B8 + channel data length
B7), Display parameters changed (rows/columns + sizing/fonts/effects, C0),
Network search mode change (manual/automatic, E5), Browsing status (browser
error code, E4) and Frames information change (default frame + per-frame info,
E7).
Group 2 (TS 31.111): IMS registration (IMPU list as 80-tagged URIs + IMS status
code), Incoming IMS data (IARI), CAG cell selection (status + mechanism, access
technology, CAG information list) and Slices status change (allowed / rejected
/ served / partially, access technology, served S-NSSAIs) - all with source
network.
Still unimplemented by design: 0x0D Local connection, 0x16 Contactless state
request (3GPP: not required) and the eCAT containers 0x19/0x1B; 0x04/0x05/0x13/
0x1A are data-free, where the one-click send is spec-correct.
Tests: the new build vectors. Help EN/RU, both AGENTS files; sw simple-v349.
MT call, Call connected, Call disconnected, Browser termination, (I-)WLAN
access status and CSG cell selection no longer fall back to the generic
"send a bare notification" prompt:
- MT call (TS 31.111 7.5.1A.2): transaction identifier, calling party number
+ TON -> Address (EFADN BCD coding, encBcdDigits/eventAddressTlv),
subaddress, IMS URI, media type; source network.
- Call connected (7.5.2A.2) / Call disconnected (7.5.3A.2): transaction
identifier, near/far-end source select, media type, and a cause - absent,
the zero-length radio-link-timeout form or custom bytes (the hex field shows
only for "custom").
- Browser termination (TS 102 223 7.5.9.2): user / error cause.
- (I-)WLAN access status (TS 31.111 7.5.1.2): no coverage / coverage /
connection on-going.
- CSG cell selection (TS 31.111 7.5.3.2): status + selection mechanism,
optional access technology, CSG ID, HNB name and PLMN ID; source network.
The device-identities source (TS 102 223 8.7: 82 terminal / 83 network) is now
selectable: EVENT_FORMS carries a fixed `src` or a `srcField`, and
_send_event_download()/`/api/event-send` take an `event_src` override (an
invalid value falls back to the terminal).
Tests: the BCD/Address builders, each event's build vectors, the "no not_yet
left" guard and the server source override. Help EN/RU, both AGENTS files;
sw cache simple-v348.
- The 0x1D event form now offers the Date-Time and Time zone object
(TS 102 223 8.39): an optional select that includes the host clock's
date-time, built by eventDateTimeTlv() as seven semi-octet BCD bytes in the
TS 23.040 9.2.3.11 order (units high, tens low) with the time zone in
quarters of an hour and its sign in bit 3 of the first semi-octet.
- The Cards list's empty-state colspan is 8 now (the table has had 8 columns
since the keyset refactor, not 10).
- The expanded-registry "(first page only)" note marks only an unfulfilled
6310: ramExpandedMore() keeps the promise set when the follow-up page was
refused/failed or the page cap stopped the paging, and clears it when an
accepted page ended the listing (a completed multi-page listing was
annotated wrongly before).
Tests: eventDateTimeTlv (fixed dates, both timezone signs), the 0x1D build
with and without the date-time, and ramExpandedMore's cases. Help EN/RU and
both AGENTS files; sw cache simple-v347.
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.
The ✕ in the Cards keyset editor was a neutral bordered button with only a
red hover; it now matches the preset list's Remove (bg-red-600 text-white,
hover bg-red-700), keeping the glyph and the "Remove keyset" tooltip.
keysets.test.js guards the red style. sw cache simple-v345.
The Cards list now marks the row whose ICCID matches the equipped card
(cardsFindByIccid handles digits / spaced / raw EF hex alike): an emerald row
tint plus a small dot with a "The equipped card's preset" tooltip in the Name
cell. There is no highlight while the ICCID is unknown (no card, unreadable
EF.ICCID) or when no preset matches - the amber banner already covers that.
pysimCardStateUpdate() re-renders the list when the ICCID actually changes, so
a visible Cards tab follows equip/removal live (opening the tab re-renders
anyway through switchTab).
Tests: cards_iccid.test.js drives cardsRender with a fake tbody - the matching
preset is highlighted (both ICCID forms), the others stay plain and no ICCID
means no highlight; card_state.test.js stubs cardsRender. Help EN/RU and the
repo AGENTS updated; sw cache simple-v344.
A keyset only exists inside a card preset, but the SCP80/RAM/TAR keyset
selectors stayed enabled with no preset selected - showing either
"— no keyset —" or a stale "Keyset N — not defined in the preset" entry left
over from an earlier preset. spKeysetSync() now disables the selector and
reduces it to a "— select a card preset —" placeholder whenever its view has
no preset selected, and re-enables/populates it as soon as one is.
Clearing the preset dropdown ("— Select card —") also refreshed nothing:
cardsApply() returned before the sync, so the stale keyset list stayed; it now
syncs the selectors (and the preset warning) on the empty selection too.
The three selects get disabled:opacity-40 disabled:cursor-not-allowed (the
rules are already in the compiled style.css). Tests: keysets.test.js covers
the disabled/cleared state and the re-enable plus a cardsApply source guard;
cards_form.test.js and cards_counter.test.js gain tarPresetIdx in their
sandboxes. Help EN/RU and both AGENTS files; sw cache simple-v343.
EVENT_FORMS[0x1D] was missing, so its Send button posted event_data: null -
the ENVELOPE carried only the event list and device identities, none of the
event's data objects. The form now builds the TS 31.111 7.5.25.2 object set
(tags per TS 101 220 / TS 31.111 9.3): data connection status (9D), connection
type (AA), optional (E/5G)SM cause (AE), transaction identifier (1C), optional
location information (13, CR flag 0 as the spec requires for this command),
optional access technology (BF), location status (9B), optional APN (C7) and
PDP/PDN/PDU type (0B).
Adjacent spec fixes found while verifying:
- the Access Technology Change options (0x0B) did not match the pinned
TS 102 223 V18.3.0 8.61 coding (GSM Compact / GSM w-EGPRS / UTRAN w-HSDPA
...); they now use the shared ACCESSTECH list, whose two labels were also
corrected (TIA/EIA-553-A, TIA/EIA-136-270);
- _send_event_download() wrote the D6 length as a single byte - a long APN
would overflow it; 128..255 bytes now use the BER long form (81 <len>).
Tests: the 0x1D build (defaults, full form, fallbacks), the 8.61 option list,
the long-form D6. Help EN/RU and both AGENTS files; sw cache simple-v342.
The Simulator view's "STK menu" block already carries the legend, so the
emerald button's hardcoded "STK: " prefix was redundant. It now shows just
the card's menu title (e.g. "Alfa Mobile"), or "Menu"/"Меню" for a
title-less SET UP MENU (`stkMenuLabel`, unchanged).
The help pages (EN/RU) describe the button as the menu title, both AGENTS
files drop the old `STK: Menu` wording, and stk_menu.test.js gains a source
guard against the prefix returning. sw cache simple-v341.
Tests: 743 Node / 619 Python.
Documentation:
- help (§1/§3): the SCP80 tab is switched by three pills - the tab list and the
section intro now name the TAR probe (RU "TAR-сканер"); §3.1/§3.2 describe the
Keyset selector next to the preset selector (and that a successful PoR
advances the counter only when SPI1 carries a counter check); §4 describes
the keyset editor rows, the derived number and the per-keyset counters; the
probe section keeps its counter rule.
- README/README_RUS: the SCP80 tab says three pills and gains a "TAR probe" /
"TAR-сканер" section (standard list, verdicts, counter persistence); the
Cards section is rewritten for the server-side store - keysets, server-owned
per-keyset counters with the audit trail, JSON export/import as the migration
path, the keyset editor, the equipped-card mismatch banner - and the field
table, "Add a card" and "Edit / remove" follow.
Terminology:
- "key version" is no longer used for SCP80 keysets in the UI, the server error
strings and the docs: the selector is "Keyset", its options "Keyset 1 - ...",
the messages say "keyset N ...". The TS 102 225 A.1/A.2 quotes and the GP
PUT KEY P1 field keep the spec's own wording.
- RU keeps the word "keyset" untranslated ('Добавить keyset', 'Номер keyset',
"не является keyset пресета", ...) and uses "пресет" instead of
"предустановка" throughout the dictionary, the help and README_RUS.
sw cache simple-v340. Tests: 742 Node / 619 Python.
Two selector bugs in the new TAR-probe view and one stale-list bug in the RAM
view, all from the lazy population of the view-specific preset selects:
- cardsAutoSelectByIccid() set sp-card-sel and ram-card-sel only, so the TAR
probe view never got the equipped card's preset (and tarExecute() then ran
with preset_id: '', i.e. without persisting the counters it consumed);
- tarRender()'s keep-fallback was -1 instead of the remembered _ramCardIdx, so
the select stayed on "— Select card —" even when the auto-selection had
remembered the preset; the TAR pill switch now also calls tarRender();
- the keyset selectors showed a stale "N — not defined in the preset":
spKeysetSync() runs during the auto-selection, when ram/tar-card-sel have no
options yet, so those lists were built with no preset; ramRender() now
rebuilds them (spKeysetSync) once its preset is known - tarRender() already
did.
Tests: the auto-select covers tar-card-sel; a DOM-driven regression test
(ramRender with a remembered preset and a two-keyset preset must show
value="2" selected and no "not defined" entry); source guards for ramRender's
sync, tarRender's keep and the TAR pill switch. sw cache simple-v339.
The probe is not a RAM operation, so it moves out of the RAM view into a third
SCP80 pill (Secured packet | RAM | TAR probe, RU "TAR-сканер"), with its own
preset and key-version selectors, its own equipped-card mismatch banner and its
own result line; the help gains a "3.3 TAR probe" section and the anchor map
follows the pill.
The TAR field is a checklist now: pre-populated with the standard allocations
of TS 101 220 V18.3.0 Annex D, Table D.1 (each row carrying the document it
comes from, all checked by default, Select all / Select none), plus custom
TARs added through a small form (three hex bytes + an optional description,
removed with a x). Custom entries live in localStorage['simple_tar_probe'] on
a best-effort basis - losing them is harmless, the standard list is always
there. Only the checked TARs are sent; nothing checked is refused with a
message. A Python test guards the PWA's list against the server's
TAR_PROBE_TARS (the endpoint keeps its own default for API callers).
RAM leftovers removed (the tar-probe operation entry, the #ram-tar-rows block,
the ramExecute dispatch, ramTarProbe); getRamSpParams() takes the view's
selector id; tarRender/tarApplyCard/tarKeysetChanged/tarKeysetGuard mirror the
RAM view; spKeysetSync and spPresetWarningRender cover the new selectors.
sw cache simple-v338. Tests: 740 Node (the checklist rows, the custom
round-trip, the checked-list read, the renderer, the pill wiring) and 619
Python (the list drift guard).
Three things learned from probing a live card, plus the capability they enabled:
1. Counter tracking follows SPI1.b5b4 (TS 102 225 5.1.1): with 00 the counter
is "present, ignored, never updated", so a packet without a counter check
must leave every counter untouched. _counter_tracked() now guards
/api/send-ota (no final_cntr, no persistence, a log line), the RAM step
sender and the test runner; _preset_counter_persist() requires a non-zero
key version (the "single keyset" fallback could advance a keyset spuriously
for a keyless send). The PWA advances the form counter only when the server
reports final_cntr and says "counter not tracked" otherwise.
2. The spec's no-security form builds again: _ota_keyset() refused the valid
KIc/KID '00' (TS 102 225 A.2: '00' is legal with SPI1.b3=0 for KIc and
SPI1.b2b1=00 for KID) because pySim wants an algorithm. An unknown nibble
is now refused only when the algorithm is actually used (ciphering / a
ciphered PoR for KIc, RC/CC/DS / a MAC'd PoR for KID) and 'implicit' codes
the nibble as 0 otherwise - POST /api/send-ota {apdu, spi1:"00", kic:"00",
kid:"00"} now builds 0D00010000B0000000000000010000A40000, the exact packet
the card accepted (a fake 15/15 was refused with 6200). A no-security PoR
is decodable again too, and a requested-but-missing PoR sets por_missing
on the plain path as well.
3. TAR probe (POST /api/tar-probe, RAM op "Probe TARs"): one secured packet per
TAR - the standard allocations of TS 101 220 V18.3.0 Annex D, Table D.1
(TAR_PROBE_TARS), or a custom list - carrying a harmless C-APDU (SELECT MF)
with the preset's keyset and SPI 16/01. Each TAR is classified
(registered / no_answer / no_por / refused) from the ENVELOPE verdict and
the application's PoR, counters advance and persist normally, and the run is
refused when SPI1 has no counter check. Live on a card: 000000, B00000 and
B00001 registered (compact-format applications, answering 6D00 / 6B00),
the expanded-format TARs, USAT, multiplexing and CASD refused with 6200 -
with the keyless form the same card accepted every TAR and answered no PoR.
Docs (api.md, help EN/RU, AGENTS), sw cache simple-v337. Tests: 618 Python
(no-security build, the algorithm-only-when-used rule, the tracking rule, the
persist guard, the probe classification) and 736 Node (the probe renderer and
wiring, spCounterTracked).
Autoselection normally keeps the SCP80/RAM preset in step with the card, so
the mismatch is easy to miss. Both views now carry an amber banner above the
preset selector (the existing warning style - no new Tailwind classes):
- the equipped card matches no preset: "The equipped card <ICCID> does not
match any card preset - the selected preset may belong to another card.
Add it in the Cards tab (From card)"; with no presets at all the banner says
so instead;
- a preset is selected but it is not the equipped card's: a softer line naming
the matching preset ("check that its keys and counters belong to this card").
`spPresetWarning(iccid, presets, selectedIdx)` is pure (unit-tested with the
ICCID forms the store accepts: digits, spaced, raw EF hex), `spPresetWarningRender`
fills both banners and runs on every card-state poll, preset load, preset
selection and page init. No card session or an unreadable ICCID shows nothing
(the card-iccid gating already explains that state); the banner warns only -
it never blocks a send (preparing a packet for another card stays possible).
Tests: frontend/tests/preset_warning.test.js (5), the card-state/preset
harnesses stub the DOM-bound renderer. sw cache simple-v336.
A card usually holds several OTA key sets, and the SCP80 counter is tracked
per key version on the card, so a preset now carries one keyset per
GlobalPlatform key version - the b8..b5 nibble of KIc/KID (TS 102 225
5.1.2/A.2), the version is implicit in the bytes. The spec is explicit:
"a dedicated counter shall be associated to each key version" (Annex A.1) and
"the key version number indicated in the KIc and KID (bits b8..b5) fields
shall be identical when different from 0" - a mismatch is rejected by the card
with "Unidentified security error" (A.2).
Server (presets.py, schema 2):
- a preset carries `keysets: [{kic, kid, kicKey, kidKey, cntr}]`; validation
refuses a KIc/KID version mismatch (A.2), a version outside 01-0F ('00'
means "no security" and is a packet-level choice, not a keyset), duplicate
versions and missing keys/counters; a v3.8.0 flat preset converts into one
keyset on load and on import;
- `set_counter(..., kvn)` writes the counter of that keyset (monotonic per
keyset), the audit line carries the kvn, and `find_keyset()` serves the
lookups;
- the operations derive the key version from the request's KIc byte and
persist into that keyset: /api/send-ota, the RAM install/install-app/compat
steps (_ram_send_gp_apdu) and the test runner's SCP80 steps (which may name
a `kvn` per step and keep one working counter per keyset);
- a non-zero key version the named preset does not define, or a KIc/KID
mismatch, is refused before anything is built (_preset_keyset_check /
_keyset_guard: 400, or a step error in the test runner). Without a
preset_id only the mismatch rule applies.
PWA:
- the Cards form's KIc/KID/counter/keys inputs become a keyset editor (one row
per version: KIc, KID, both keys, that version's counter, Add/Remove; the
version is shown derived from the bytes and a mismatch is flagged);
- the SCP80 and RAM views get a Key-version selector beside the preset
selector, driving the KIc/KID bytes and loading that keyset's keys and
counter; the packet builder refuses a version the preset does not define
(spKeysetCheck/spKeysetGuard, pure and unit-tested);
- the counter syncs are per keyset (spCntrLocalSync/ramSaveCntr), the SCP80
form's counter edit posts the keysets, and the test-run write-back syncs
every counter the server reports (scp80_counters).
Docs/help/AGENTS updated; sw cache simple-v335. Tests: 610 Python (store
keysets, endpoint refusals, the counter landing in the right keyset) and 726
Node (keysets.test.js plus the updated harnesses).
- /api/send-ota no longer 500s on a counter-less body: the plain `sp` path
reports final_cntr only when the counter is usable (_counter_valid, 1-10 hex
digits) - before, the new plain-path report ran int('') and the request died
in the generic handler. The failure path is guarded the same way.
- _decode_por wraps the keyset/SPI construction in the same "never raise" rule
as the decode: a malformed counter or missing keys return None instead of
escaping the handler.
- presets.add() never accepts a taken id (a fresh uuid is assigned, like the
import path), the counter is validated as 1-10 hex digits instead of being
silently masked/kept, and set_counter() ignores a malformed value with a log
line instead of writing it.
- the preset endpoints answer a JSON 500 when the store write fails (read-only
home, disk full) instead of dropping the connection.
- tests: counter-less send-ota, store-write 500, duplicate id, counter
validation (add/update/set_counter), and the temp-file check no longer
matches the audit file; docs/api.md documents final_cntr for the plain path
and preset_id.
The presets lived in the browser's localStorage and the PWA wrote the SCP80
counter back after every operation, so an increment could be lost when the
response never reached the tab, when two browser contexts (or the demo origin
plus a local one) held separate copies, or when the tab was closed mid-run.
The counter is card state and the server performs every card operation, so the
store now lives next to the server:
- pysim_simple_server/presets.py: one JSON file
(~/.pysim-simple-server/card_presets.json, --card-presets overrides; Path.home()
resolves the same way on Windows), atomic writes (temp + fsync + os.replace),
an RLock, stable uuid ids, the ICCID normalisation the PWA uses (digits /
spaced / raw EF hex), a duplicate-ICCID refusal and a monotonic set_counter;
every counter change is appended to card_presets.json.audit.jsonl.
- API: GET /api/presets (card-free), POST /api/presets and
/api/presets/update|delete|import; the import also takes the old localStorage
export shape - that is the migration path, there is no automatic migration.
- Operations name a preset_id and the server persists the counter they
consumed: /api/send-ota (the plain SCP80 path reports final_cntr now too),
the RAM install/install-app/compat steps (_ram_send_gp_apdu) and the test
runner's SCP80 steps (_preset_counter_persist).
- PWA: the Cards tab is a thin client of the store (load on connect, CRUD via
the API, export/import), the counter write-backs became local display syncs
(the SCP80 form's counter input stays a deliberate human edit) and the
SCP80/RAM bodies carry preset_id; a new pysimFetchOk() surfaces the server's
error bodies instead of silently using them.
Policy: card keys in logs and local files are not a risk for this workbench
tool (AGENTS.md) - the store is not encrypted and no redaction is required.
Tests: tests/test_presets.py (store: ICCID forms, counter monotonicity and
wrap, atomic writes, corrupt-file recovery, import), the preset endpoints in
tests/test_fetch_routes.py (incl. the counter persistence via /api/send-ota)
and the updated PWA tests. sw cache simple-v334.
start.sh tees its console (exec > >(tee ...)) and exports PYTHONUNBUFFERED
so the log stays live; start.bat does the same through PowerShell
Tee-Object (UTF-16 on PS 5.1, UTF-8 on PS 7+). The log carries card keys
and request payloads of the test cards, so it is gitignored and must not
be committed - read it for the latest server output instead of
copy-pasting the console.
Two bugs in the v3.7.0 per-AID fallback, both visible in the live log
(2026-09-30 11:21):
- the candidate applet was searched by the AID prefix, but the applet
instance AIDs are the package's module AIDs (package A113...0200 has
modules ...0208 / ...494D4508, both present as applet instances - they
differ from the package AID in the last byte), so that package produced no
query at all. ramElfAppletCandidate (pure) prefers the module AIDs and
keeps the shared-prefix rule as a fallback;
- the single-AID expanded query answered 6134 (the card holds the data) and
the empty response was read, so no version was derived. The query is now
retried with the GET RESPONSE chained into the compact command string
(ramChainGetResponse) exactly like the ladder - a standalone GET RESPONSE
answers 6700 on this card.
Tests: the module->applet mapping with the live AIDs (incl. the versioned
applet being skipped and the prefix fallback), plus the source guards for the
chained retry.
sw cache simple-v333; tests: 557 Python / 717 Node.
No live card accepts a tag list for the ELF queries (tags/noea/lean all
answer 6A88 on P1=10 - the log of 2026-09-30 11:11), so the version-carrying
CE tag was unreachable there; the ELF listing itself (notags + chained GET
RESPONSE) works and carries AID + life cycle + modules only.
Two fallbacks:
- when the ELF rows have no version and the details pass never got a
tag-list form, the ladder is walked once more with P1=20 (a card's P1=10
implementation can be stricter) and merged by AID;
- any version still missing is fetched per applet AID: the AID search
qualifier (4F <len> <AID>, GP 11.4.2.3 Table 11-35) plus the apps tag
list selects a single applet entry, whose CE is its package's version
(the applet <-> package mapping is the AID prefix; at most 6 queries).
Also:
- the app and ISD rows show their parsed CE version (the expanded apps
listing carries it);
- an unfulfilled 6310 (the next-occurrence page refused, e.g. 6A86) is
reported as '(first page only)' instead of looking complete;
- the page loop only continues when the chained GET RESPONSE was accepted -
a refused retry no longer costs an extra P2=03 packet.
sw cache simple-v332; tests: 557 Python / 716 Node.
A cntr_low PoR verdict is not recoverable by retrying - every packet whose
counter is not above the card's is rejected again - so it now stops the
operation and is stated as plain data instead of a generic PoR error.
Server:
- _cntr_low_fields(status, cntr) -> {cntr_low, card_cntr, suggested_cntr}
(fixed-width wrap like the counter field); carried by /api/send-ota (plus
an 'OTA CNTR-LOW: card counter ...' log line) and by the RAM install /
install-app / cap-compat failure responses (from the failed step);
- _send_envelope's late-PoR poll now also runs after a warning ENVELOPE
answer (62xx/63xx - the live card answers 6200 for a low counter) and
_send_secured_packet passes the captured PoR on, so the verdict is named
instead of a bare 'ENVELOPE failed' (that is why it only surfaced on the
later SPI2=0x21 query in the live log).
PWA:
- spCntrLow(res) guards every flow; the Explore routes the whole run through
a checked sendOta(), so the paginate/expanded loops stop, no further
packet is sent and no counter is written back (delete and Send to Card
check it too);
- the result states the card's counter and offers 'Go to preset'
(ramGoToPreset: opens the failed operation's preset in the Cards tab for
editing and focuses the counter field) - nothing is written
automatically; the operation modal carries the same notice;
- help (EN/RU) documents the guard.
sw cache simple-v331; tests: 557 Python / 714 Node.
The live 500 in the Explore ('OTA send error: index out of range') was a
parser bug, reproduced with the live FETCH shape: _parse_proactive_header
walked the command TLVs reading the length as a single byte, so the card's
SEND SHORT MESSAGE with its 150-byte SMS TPDU (8B 81 96) was mis-walked
into the TPDU and eventually indexed past the end (val[2] on a short
slice) - the FETCH was left unanswered and the request answered 500.
- _ber_tlv_list/_proactive_body helpers: BER lengths (_ber_len_at) plus a
bounds check that stops at a truncated TLV instead of walking into the
value; _parse_proactive_header, _parse_display_text, _parse_select_item,
_parse_setup_menu_items, _tlv_map and _decode_poll_negotiation all use
them now (the same latent bug for any long-form TLV);
- _run_proactive_chain guards every step (header parse, proactive log,
on_fetch, BIP/timer handlers): an exception is logged with the offending
FETCH bytes and the TERMINAL RESPONSE is still sent - a decoder bug can
never leave a FETCH unanswered again;
- the expanded-registry report distinguishes a request error from a remote
SW: 'ELFs: request error (index out of range) - compact listing used'.
Regression tests: the live long-form FETCH shape parses (header + TLV map +
TPDU), truncated TLVs stop the walk, and a raising parser still gets a TR.
sw cache simple-v330; tests: 553 Python / 711 Node.
The live log (2026-09-30) showed our standalone GET RESPONSE was wrong: the
card answered 6700 (wrong length) to 00C00000<xx> for the 61xx answers of
the expanded ISD query and the ELF notags query. The compact path proves
how this card wants it: GET STATUS and GET RESPONSE chained in one compact
command string (80F2..C0000000).
- a 61xx answer now retries the same query with ramChainGetResponse
(+ C0000000, compact format only; the expanded AA/22 form has no command
string, so no retry there);
- a 61xx page still continues to the next occurrence (the GET RESPONSE's own
SW replaces the 6310 signal); the ISD stays a single occurrence;
- the standalone 00C00000<xx> attempt is gone (it only ever answered 6700).
This card also refuses every tag list for the ELF queries (tags/noea/lean
all 6A88), so CE cannot come from the ELF listing. Its apps listing does
carry the load file version (CE) together with the package AID (C4), so
ramDeriveElfVersions fills a missing ELF version from its applet instance
and the row annotates it: 'Version: 9.3 (from the app instance)'. The
version hint now shows whenever any ELF row lacks a version.
sw cache simple-v329; tests: 550 Python / 711 Node.
The complete live trace (2026-09-30) showed the card was not refusing nearly
as much as the report suggested:
- the ISD expanded query answered 6131 and the ELF notags query 617F/613F -
response data pending, and no GET RESPONSE was ever sent;
- the apps expanded query answered 6310 (more occurrences) and only the
first page was merged;
- the ELF tag-list forms (tags, noea) answered 6A88 because they still
carried the application-only C5 privileges / C4 ELF AID.
Fixes:
- 61xx -> ramGetResponseApdu(sw) = 00C00000<xx> (up to 4 rounds);
- 6310 -> the same query as a next-occurrence page (P2.b1=1, P2=03, up to
8 pages), so the apps listing is complete;
- a 'lean' ladder form with only the objects valid for the kind: ELFs get
4F 9F70 CE 84 (AID, life cycle, version, modules), the ISD/apps get
pySim's 4F 9F70 C5 CC; the ladder is tags -> noea -> lean -> notags;
- the report groups by listing label with the best outcome winning
(entries > accepted-but-empty > refusal): the P1=10/P1=20 ELF double
query no longer prints 'ELFs, ELFs' and a pass with data is never called
a refusal; the ELF version hint is form-aware.
sw cache simple-v328; tests: 550 Python / 709 Node.
The 'Fetch expanded registry details' checkbox is now on by default: the
expanded pass carries the package versions, the SD/load-file associations
and the applet menu entries; an explicit stored opt-out is still honoured.
Live 2026-09-30: a card with a non-default ISD AID answered 6A88 to the
reference expanded query (P2=02 + the tag list), so the ELF rows had no
versions. Cards differ in what they accept, so the query is now tried in
three forms (RAM_EXPANDED_FORMS):
tags - the reference tag list (incl. the TS 102 226 EA SCP Registry Data)
noea - the same list without EA (a card that errors on it)
notags - no tag list at all (GPPro's 4F00-only data field); the card
builds the default E3
The first form that answers is remembered for the run and tried first for
the later listings; the ISD gets a single attempt - its 6A88/6A86 is normal
('no data to report', GPPro) and must not poison the detection for the
apps/ELF queries.
Every listing's outcome is reported in the explorer (ramExpandedReport):
'Expanded registry: tag list + EA (Apps, ELFs) - ISD: no expanded data
(6A88) - compact listing used', or 'Expanded registry refused by the card -
compact listing only (package versions unavailable)'; the ELF section hints
when no version came back. Tooltip/help (EN/RU) updated.
sw cache simple-v327; tests: 550 Python / 707 Node.
ramShowInstallResult appends ramCompatDetailHtml(data, capMem) to the
install modal (the modal content is HTML now - the step lines are escaped
and ramOpModalFinish takes the asHtml mode), so a failed LOAD / INSTALL
[for load] shows the same block analysis as the compatibility test:
- the failing block's CAP section (load-file byte range and components, and
what completes there), with the JC VM spec 4.5.2 note - or 'the import
list already passed' when the failure is later in the file;
- the remote status word with its name;
- the import list at the CAP's own versions (a real install runs no probe);
the closing hint points at the compatibility test ('Set all 0.0' first,
then bisect), while a probe run keeps the lower-the-versions wording.
ramInstallFailHint stays as the short inline hint; INSTALL [for install] and
/api/ram-install-app failures show no block (the LOAD guard in the helper).
sw cache simple-v326; tests: 550 Python / 704 Node.
The compatibility test now runs in the same operation modal as the installs
(live progress from the server's ram_progress, Close disabled until done) and
its result gains a per-AID detail block instead of the comma list:
- the probe summary (probe: 2 imports -> 0.0 . +1 appended);
- the import list as tested (ramCompatImportRows): the CAP's imports with the
version the card was asked for and the CAP's own version when the probe
lowered it, appended synthetic entries and unmatched lines - each with the
resolved library name (jcAidName), so a raw AID list becomes readable rows;
- on a rejection, the failure analysis (ramCompatFailureHtml): which CAP
section the failing block covers (load-file byte range and component names,
'completes Import'), what the card checks at that point (JC VM spec 4.5.2)
or that the import list already passed, the remote status word with its
name, and what the probe state implies (every import at 0.0 -> a missing
package or another load-file check is the cause; otherwise lower the
versions and bisect).
ramCompatVerdict names the remote SW; the inline result keeps the verdict +
short probe note and the modal carries the escaped step lines + the detail
HTML (ramOpModalFinish gained an asHtml mode); the server reports
probe_unmatched as [{'aid', 'version'}] (_probe_unmatched), documented in
docs/api.md. The same failure block for the real install is the next step.
sw cache simple-v325; tests: 550 Python / 703 Node.
The compact (legacy) GET STATUS entry is <aid_len><AID><life cycle>
<privileges> + (P1=10) <module count> (<len><module AID>)*, cross-checked
against GlobalPlatformPro's populate_legacy (the 4th byte is privileges;
0x00 is required for load files). The parser read it as a 'version' one
byte late, so ELF rows showed the module count / the next entry's length as
Version: 00/10 - the compact listing has no version at all; it now comes
from the expanded CE tag (ramFmtVersion: 2 bytes -> major.minor, GP Table
11-37 Note 1) and merges (the bogus compact value no longer blocked it).
New data exposed:
- EA SCP Registry Data (TS 102 226 8.2.1.6) -> 80 Menu Parameters -> the
applet's STK menu entries: STK menu entry: 0x80 (position 1, Enabled)
- unknown registry objects are preserved (rawTlvs) and shown as
'Other registry data: 8F=752B . 90=012D ...' instead of being dropped
- 84 parses the GP coding (one Executable Module AID per TLV, repeated; the
concatenated 4F<len><aid> form stays as fallback) and the ELF expanded
queries add 84 to the tag list (with a tag list the card returns only the
listed tags - the fallback path would otherwise lose the module lists and
their Install buttons)
- life cycle names decode per object kind (GP Tables 11-3..11-6: ISD/card,
app, SD - detected via the Security Domain privilege - and ELF), with
multi-byte 9F70 (02 0700 on live cards) decoding the first byte and
surfacing non-zero extras; the R-APDU parser's LIFECYCLE_MAP labels were
aligned with the same tables
/api/ram-install measures the NV footprint: a silent GET DATA FF21 read
before and after the chain (also on failure) returns nv_before/nv_after/
nv_delta; the reads never fail the operation (no FF21 -> no fields) and the
PWA shows 'NV used: ~N B (free A -> B)' in the install result.
sw cache simple-v324; tests: 548 Python / 698 Node.
The SIM/UICC Toolkit parameters grid is grid-cols-3 now: the 12 narrow
fields (priority, timers, text length, menus, first pos/id, last pos/id,
channels, MSL, TAR, access domain) fit in four clean rows, Max services
keeps its own row (it was a lone half-row in the 2-column layout) and the
file-access group is one col-span-3 row: File system access (full) ·
ADF.USIM access (full) · ADF AID (hex) + From card.
The ADF AID cell is its own rc-tk-adfaid-row; the whole group is wrapped
in rc-tk-access-row, which updateRcTkMode hides in SIM/CA mode (hiding
only the old checkbox rows would leave the AID field behind).
grid-cols-3/col-span-3 were already compiled (no Tailwind rebuild); the
html test pins the 3-column grid and the spanning access row with its
three cells, the JS guard pins the CA/EA visibility.
sw cache simple-v323; tests: 540 Python / 689 Node.
Review of v3.6.48 found six issues; the two that matter in use:
- ramProbeParse('') errored, so the basic 'is this CAP compatible with
this card at all?' run (no overrides) was refused by an alert. An empty
list is valid now: the test runs with the CAP's own versions.
- ramCompatVerdict said 'Import list rejected' for every failure. It now
distinguishes: a request error (no steps), a failed format probe (the
card not answering), an INSTALL [for load] refusal, and only then the
import list - each with its own headline.
Plus:
- the result line shows the applied probe (ramCompatProbeNote: 'probe:
7 imports -> 0.0 · +1 appended', unmatched AIDs included);
- #ram-compat-block validates 1..240 like the install's block size;
- the server reports probe_unmatched (override AIDs matching no entry,
previously silent when another override matched) and logs them;
- tests: the endpoint's probe-400 path (a mini CAP + a dummy scc),
_cap_import_aids, the verdict cases and the probe note.
sw cache simple-v322; tests: 540 Python / 687 Node.
The import probe no longer lives in 'Install Package (.cap file)': a
diagnostic that can leave a LOADED-only package does not belong in the
operation meant to produce a working applet. /api/ram-install no longer
takes probe_imports; the install form's probe fieldset is gone.
New RAM operation CAP compatibility test (POST /api/cap-compat): the
server runs the format check, INSTALL [for load] and the LOAD blocks only
up to the block that completes the Import component (_cap_import_end +
_cap_compat_blocks, computed from the load file and the C4 TLV framing),
then stops - no last-block flag, no INSTALL [for install], so nothing is
committed and nothing needs deleting. The verdict (imports_ok,
boundary_block/total_blocks) states it covers the LOAD/import gate only;
the link gate stays the real install's business. Verified on the real
RemMobileID CAP: the boundary is LOAD 1/64, 2/128, 3/240 for 240/120/64 B
blocks - exactly the blocks where the live cards rejected the load.
_probe_import_versions(load_file, overrides, additions) now supports
append-only synthetic imports: +AID=version appends a new entry, the
Import component's size field and the Directory's Import size are patched,
and existing entries keep their positions (constant-pool import indices
stay valid); an AID that is already imported becomes a version override.
This is the card-capability query ('does this card export package X at
>= Y?') without building a probe applet.
PWA: new op section (CAP file + analysis + the version list + block size),
ramCompatTest with inline results and ramCompatVerdict, ramProbeParse
understands +AID=version; help EN/RU documents the operation, the two-step
bisect and the gate scope.
sw cache simple-v321; tests: 538 Python / 686 Node.
The install modal's progress bar froze around 70% and jumped to 100% at
the end: not a math bug - the UI's other polls (/api/stk-status,
/api/poll-status, /api/proactive-log, /api/menu, ...) were card-locked in
do_GET even though they only read cached state, queued behind the install
and exhausted the browser's 6-connections-per-host budget, starving
/api/status. _CARD_FREE_GET lists every card-free GET (status, version,
stk-status, poll-status, proactive-log, menu, events, terminal-profile,
pli-*, commands, net-state, mcc-mnc, scp81/bip status|script|log);
/api/cardinfo and /api/esim/* stay locked (they touch the card).
tests/test_fetch_routes.py holds _CARD_LOCK and asserts the cached
endpoints still answer - verified to time out on the pre-fix list.
The install modal is wider (max-w-3xl) and successful step lines are
compact: '✅ Step 3: LOAD (1/240) - 6101 · 274 B / 3 SMS' (no verbose SW
name, B instead of bytes, the SMS count only when >1); failed steps keep
the full detail and the hint.
sw cache simple-v320; tests: 533 Python / 684 Node.
The max-w-7xl mx-auto page container closed before #tab-scp81, so SCP81
and Cards rendered directly under <body> and stretched to the whole window
while the other five tabs were capped at 1280 px. The closing </div> moved
after #tab-cards (before the modals); a structural test walks the markup
and asserts every tab-content has the container as an ancestor (verified to
fail on the old nesting with 'tab-scp81 must be inside the max-w-7xl page
container').
The SCP80/RAM form's Card preset and Operation selects now share one
flex gap-2 items-end row with Execute (guarded by a markup test).
sw cache simple-v319; tests: 531 Python / 683 Node.
- Explore runs inline: no blocking modal (that stays for the install
operations, which can leave a half-loaded package; Explore leaves the
card consistent). A test guards that ramExplore never calls the modal.
- Minimal success line: just OK - the details live in the explorer below;
error runs keep the detailed 'Partial - ...' line.
- Counts moved into the section headings (Applications / Applet Instances
(n), Executable Load Files (ELFs) / Packages (n)); the redundant '1 ISD'
count is gone (P1=80 returns the ISD as a single occurrence, GP
11.4.2.1).
- The RAM command format line gained a tooltip: compact = TS 102 226
5.2.1 command string, expanded = the AA/22 scripting template (5.2.1.0),
detected per operation with a read-only GET STATUS probe.
- The expanded-registry checkbox is relabelled 'Fetch expanded registry
details (load-file / SD associations)' with a tooltip (extra secured
packets, card support varies, the fallback is automatic) and is now a
sticky per-browser preference (simple_ram_expanded).
sw cache simple-v318; tests: 531 Python / 681 Node.
The CAP details' Platform row named only the Java Card level; it now also
shows the ETSI/3GPP release the UICC API imports come from, with its basis
in brackets, e.g. 'Java Card >= 2.2.2 - ETSI REL-7 (uicc.toolkit 1.4) -
CAP format 2.1'.
capmem.ETSI_TOOLKIT_RELEASE maps the uicc.toolkit export version to the
release, sourced from the workspace corpus: sim-tools/ETSI-REL.md for the
prepared trees (api21_rel7_export_files carries uicc.toolkit 1.4 = the
REL-7 jar, api21_export_files 1.11 = the REL-12 jar) and
sim-tools/etsi/102241/README.md's package-version table for the rest
(1.0 -> REL-6/7, 1.3 -> REL-8, 1.5 -> REL-9, 1.12-1.14 -> REL-15..18; the
1.11 spec range is 13.01-14.02 but the tree the builds use is REL-12).
Exposed as requires_etsi_release/requires_etsi_basis in /api/cap-info and
the CLI; SIM-only CAPs (no uicc.toolkit) get no label.
Verified: RemMobileID -> REL-7 (uicc.toolkit 1.4), AlfaDSA -> REL-12
(uicc.toolkit 1.11). sw cache simple-v317; tests: 531 Python / 677 Node.
The probe's checkbox described the mechanism ('Rewrite the CAP's import
versions before the load'); it now reads 'Enable import probe' (purpose
first) with the mechanism in a tooltip, and the field carries two short
hint lines: what the card checks while loading, and the workflow (Set all
0.0 first; if the LOAD passes the list was the cause, then restore one
line at a time to find the package/version the card lacks).
Static tooltips are translatable now via data-l10n-title (handled by
translatePage, checked against LANG_RU by html.test.js together with
data-l10n); the help's RAM-install section documents the two-step
workflow (EN/RU).
sw cache simple-v316; tests: 530 Python / 676 Node.
Cards that cannot satisfy the CAP's import list reject the LOAD as soon
as the Import component is complete (live 2026-09-29: 6438/6985 in the
block containing its last byte, on several cards incl. an eUICC profile
whose ISD is not the default AID and which does not answer GET DATA 0066 -
no direct platform-version readout exists).
_probe_import_versions (pure) rewrites the Import component's two version
bytes per entry in the assembled load file: same length, component sizes,
Directory and offsets untouched; unknown AIDs and bad versions are
rejected. /api/ram-install takes probe_imports ({'all': '0.0'} or
per-AID), echoes the applied changes in the response and logs them. The
PWA's RAM install form gains a collapsed Import probe fieldset (checkbox,
AID=version lines prefilled from the CAP analysis, 'Set all 0.0') and the
result line reports the applied changes. Diagnostic only - never used for
a real install.
Also verified _cap_parse's component order against the reference TCA load
trace (Header, Directory, Import, Applet, Class, Method, StaticField,
ConstantPool, RefLocation, Descriptor - Import before Applet, not
ascending tag order), so the order is correct and the import list remains
the prime suspect.
sw cache simple-v315; tests: 530 Python / 676 Node.
pysimFetch() sends POST whenever a body is passed, but /api/test/run,
/api/test/stop and /api/test/clear were registered in _do_GET (since the
phase-1 commit 3a4a098): every Start fell through to the catch-all
404 {'error': 'Not found'} and the feature had never been exercised over
HTTP (a browser GET would not carry the JSON body either).
The three branches moved into _do_POST; safe under the RLock-based
_CARD_LOCK because _test_run_start re-acquires it before spawning the
worker.
New tests/test_fetch_routes.py:
- a PWA<->server route/method matrix (every body-carrying pysimFetch needs
a POST route, body-less calls a GET/POST route; routes are extracted from
_do_GET/_do_POST incl. the 'self.path in (...)' and startswith forms) -
verified to fail on the pre-fix routing with exactly the three endpoints;
- an HTTP smoke test on the real handler: POST /api/test/run {} -> 400,
POST stop/clear -> 200, GET /api/test/run -> 404.
sw cache simple-v314; tests: 527 Python / 675 Node.
The TERMINAL PROFILE chain parsed the SET UP MENU title with 'tag == 0x05'
only, so the live Alfa card's title (CR-set 0x85, 'Alfa Mobile') was
dropped: /api/menu returned items without a title and the PWA showed
'No menu set by the card' - card-specific only because cards pick the tag
variant (this one sends plain 0x8F items and a CR-set title in the same
command).
- _parse_setup_menu_command (pure, tested with the live FETCH bytes)
replaces the inline parse; the PWA shows a title-less menu as 'STK: Menu'
instead of 'No menu set by the card' (stkMenuLabel, pure).
- _cmd_tlv is now the single lookup helper: exact tag, then tag ^ 0x80
(bit 8 is the CR flag, TS 101 220 7.1.1); the old '| 0x80'-only helper and
the duplicate _cmd_tlv_any are gone.
- Remaining single-style checks fixed: _parse_setup_menu_items (0x8F/0x0F),
_parse_select_item title (0x85/0x05), _find_sms_tpdu (0x8B/0x0B - the
plain variant lost the SEND SHORT MESSAGE PoR TPDU), the response
scripting parsers (R-APDU 0x23/0xA3, executed count 0x80/0x00) and the
FF21 resource tags (0x81-0x83/0x01-0x03).
- tests/test_tag_variants.py flips the CR flag on every top-level tag of
real command fixtures and asserts identical parse results for all
parsers (verified to fail when a single-style lookup is reintroduced).
sw cache simple-v313; tests: 524 Python / 675 Node.
__main__ built a plain HTTPServer (single-threaded): during a RAM install
POST every other request waited in the accept queue, including the 2 s
/api/status poll that feeds the operation modal - its bar and step
headline never moved and the whole result appeared only at the end (live
2026-09-29).
_build_http_server() now returns a ThreadingHTTPServer (daemon threads).
Card access is unchanged: it is serialized by _CARD_LOCK, not by the
server - every POST is blanket-wrapped in do_POST, every /api/ GET except
/api/status and /api/test/status is wrapped in do_GET, and the background
threads (STATUS poll, _bip_data_available, test runner) take the lock;
the audit found no card-access holes. do_GET also lets non-/api (static
PWA) paths bypass the lock, so page assets stay answerable during long
operations.
Regression test tests/test_http_concurrency.py: a fast request must
complete while a slow one is in flight (fails on HTTPServer with exactly
the reported symptom: 1.01 s vs 0.20 s); test_startup_cardless patches
the new factory.
sw cache simple-v312; tests: 519 Python / 674 Node.
The menu chain paused only for DISPLAY TEXT / SELECT ITEM / SET UP MENU,
so a GET INPUT was auto-answered with a bare success TR and no Text
string - the live RemMobileID PIN prompt got an empty answer.
Server: _parse_get_inkey/_parse_get_input read the prompt, qualifier
flags (digits-only, UCS2, Yes/No, immediate, hidden, packed, help),
Response length 91 and Default Text 9D; the fetch handler pauses with
them; _menu_send_response(..., text=) validates the answer (digits set,
min/max, one char for GET INKEY) and builds the Text string TLV 8D in the
requested coding (SMS unpacked 04 / UCS2 08 / packed 00 via _pack_gsm7),
Yes/No as 01/00, empty input as the null text string, help as result 13
and the variable-timeout Duration echoed (6.8.4). /api/menu-respond
takes text; /api/stk-status carries pending_data so a card-initiated
prompt opens the panel; _decode_cmd decodes both commands.
PWA: stkInputHtml/stkInputHint/stkInputValidate render the constrained
panel (password field for hidden entry, numeric keyboard, default text,
Yes/No buttons, help, immediate-digit auto-submit); OK submits through
stkMenuOk; the poll opens the panel for card-initiated prompts.
sw cache simple-v311; tests: 517 Python / 674 Node.
_send_envelope defined its SEND SHORT MESSAGE capture handler inside the
91xx branch, but the 'STATUS poll (PoR not captured)' path used it: a
packet whose ENVELOPE answered 9000 and whose late PoR arrived on the
STATUS poll raised UnboundLocalError, returned 500 and aborted the RAM
operation mid-packet with the card's command left pending (live
2026-09-29, 120-byte LOAD blocks). The handler is now defined for every
path, and the late-PoR poll runs only after a packet's last segment
(poll_status from _send_secured_packet) - a concatenated download makes
no spurious mid-packet STATUS exchanges.
The PWA annotates every LOAD step with the CAP components its block
covers (ramStepComponents: 'completes Import' / 'inside Method' /
'Header...Import') and ramInstallFailHint names the component the failing
block completed - the live import-verification rejection lands in the
block completing Import (6438/6985 at both 240 B and 64 B blocks).
sw cache simple-v310; tests: 509 Python / 672 Node.
capmem derives the minimum platform from the CAP's Import component
(javacard.framework -> Java Card level via the SDK-kit corpus, int flag)
and /api/cap-info exposes it; the CAP details show it as a Platform row
('Java Card >= 2.2.2 - CAP format 2.1 - int support').
A failed RAM install at a LOAD / INSTALL [for load] step now appends a
hint naming the import requirement and the smaller-block fallback
(ramInstallFailHint, pure): JC VM spec 4.5.2 makes every import a
'>= version' demand on the card's own packages checked while the load
file is verified - live 2026-09-29, a CAP requiring Java Card 2.2.2 was
rejected exactly at the block containing the end of Import (6438 at
240 B blocks, 6985 at 64 B), not at a step count.
sw cache simple-v309; tests: 507 Python / 670 Node.
The live log showed the "stuck" state: after the menu watchdog (60 s)
answered a pending command, the PWA kept the panel; the next click got
{'error': 'no pending command'}, 400 and that reply was fed to the dialog
handler as if it were a card answer. A page reload then cancelled in-flight
polls, flooding the log with BrokenPipeError tracebacks.
- stkMenuRespond detects the already-answered reply (stkNoPendingError) and
closes the panel (stkMenuPanelReset) instead of treating it as an answer;
the 5 s status poll drops the panel whenever the server reports
pending: false, even when the Phone tab is hidden.
- /api/terminal-profile (POST) and /api/rescue finish a pending command
with a cancel TR first, so re-sending the TP can never shadow an
unanswered FETCH.
- _send_json and _serve_static swallow BrokenPipe/ConnectionReset/
ConnectionAborted with one "CLIENT GONE" line (a reload cancels polls);
the handlers' state changes still stand.
Tests: stkNoPendingError + the already-answered respond path; version
3.6.35; sw simple-v308; stylesheet rebuilt.
A CAP install is a long chain of LOAD commands whose progress and results
were only visible at the bottom of a large form - and an interrupted
install can leave a half-loaded package.
- Server: _RAM_PROGRESS (+ _ram_progress_begin/step/end) records the live
step of the running RAM operation; /api/ram-install and
/api/ram-install-app wrap their chains in it (total = steps + 1 for the
format check) and /api/status carries it as ram_progress - lock-free, so
the 2 s poll works while the request holds _CARD_LOCK.
- PWA: #ram-op-modal (the STK panel pattern) blocks everything while a RAM
operation runs: title, spinner, status line, progress bar, per-step
result lines, Close disabled until it finishes; a beforeunload guard
protects the window during the run. install-cap, install-app /
make-selectable and Explore all use the shared helpers
(ramOpModalBegin/Status/Finish); the install results stay in the form
area as before once the modal is closed. The status poll feeds the modal
via ramOpProgress (headline from the pure ramOpProgressText).
- Tests: ramOpProgressText (JS) + the progress lifecycle (Python); the
card-state and capmem suites stub the DOM-bound hooks.
- Stylesheet rebuilt; version 3.6.34; sw simple-v307.
The suggested quota row mixed a decimal C6 with hex C7/C8, so C6=27246
could be misread as hex (about 6x off). It now renders as one consistent
line via the pure capQuotaText(sug): decimal bytes with the hex in
parentheses (C6=27246 B (0x6A6E) . C7=270 B (0x010E) . C8=5143 B (0x1417)),
with a tooltip naming the tags (C6 = non-volatile code space / load file,
C7 = volatile RAM, C8 = non-volatile data).
The static "Notes" disclosure (NVRAM definition + "estimate only") is
removed from the CAP details box - the help (EN/RU, the Install Package
row) carries it now; only the dynamic scan warnings (e.g. unknown opcode)
stay visible, since they mean the estimate is partial.
Tests: the new formatting pinned, the removed notes asserted absent;
stylesheet rebuilt; 664 frontend / 504 Python green; version 3.6.33;
sw simple-v306.
The RAM install form's toolkit parameters are the make-or-break field for
toolkit applets, and CA vs EA is easy to get wrong (the earlier live 6A80s).
- "SIM/UICC Toolkit parameters" is now an accented bordered fieldset (blue
border/legend, always visible in the cap / install / make-selectable
modes) with the checkbox in the legend and the fields inside.
- ramCapToolkitMode(imports) maps the CAP's linked libraries to the install
parameter family: sim.* (AID tree A0000000090003FFFFFFFF89...) -> SIM
Toolkit (Tag CA), uicc.* / uicc.usim.* (A0000000090005FFFFFFFF89...,
A0000000871005FFFFFFFF89...) -> UICC Toolkit (Tag EA) - family-based, so
access-only applets get the right field too. Applied when the block is
enabled and when the CAP is swapped while it is on, with a "detected from
the CAP" hint under the select; no CAP keeps the manual choice.
- Rebuilt style.css for the new accent classes; tests pin the fieldset and
the family mapping.
663 frontend / 504 Python green; version 3.6.32; sw simple-v305.
Even with the rebuilt style.css, three caching paths could still serve the
old file:
- the server sent Cache-Control: no-cache only for index.html/sw.js, so
browsers could heuristically cache style.css;
- the SW install precached with addAll(), which re-uses HTTP-cached copies,
so a new SW version could precache the stale file;
- the fetch handler is cache-first (fine once the cache is fresh).
Now every static shell file is served with Cache-Control: no-cache plus
Last-Modified and cheap 304 revalidation, and the SW install fetches each
precache URL with cache: 'reload' (CACHE simple-v304). A test pins the
install behaviour.
661 frontend / 504 Python green; version 3.6.31.
Note: the public demo simple.atroshin.ru is still v3.6.20 with the old
stylesheet - it needs a deploy of the current frontend/.
The compiled Tailwind stylesheet (frontend/style.css) had been stale since
2026-09-22: classes introduced later were missing from it, so the Install /
Make selectable chips had no background or text colour in light mode (dark
mode only looked green because an older dark:text-emerald-400 rule matched)
and the new w-28 action column did not apply.
- Rebuilt with `npm run build` (Tailwind + appended src/contrast.css);
verified the previously missing classes (bg-emerald-100, text-emerald-700,
hover:bg-emerald-200, bg-red-100, bg-blue-100, w-28, ...).
- Explorer rows are visible chips now: rounded bg-gray-50 dark:bg-slate-800/60
with a hover:bg-gray-100 highlight and the colored left accent kept (module
rows a subtler variant), so a right-column button is unambiguously tied to
its row; ISD/app/ELF/module rows all use the same treatment.
- AGENTS working conventions: an index.html change that introduces new
utility classes also requires `npm run build` in frontend/.
660 frontend / 504 Python green; version 3.6.30; sw simple-v303.
The explorer buttons sat inline after each AID/detail line, so their columns
moved with the text length. Now every entry (app / ELF) and every module
line is a row with a fixed-width action column (w-28):
- left cell: the text (module rows are siblings of their ELF entry, indented
pl-6, so their buttons share the same column as the entry buttons);
- right cell: the buttons, full width of the column, so Delete / Delete All /
Make selectable / Install line up vertically across rows.
All buttons go through ramActionBtn(label, action, tone): Danger (red) for
Delete / Delete All, Ok (emerald) for Make selectable and the **Install**
buttons, which were blue. Test pins the column width and the Install tone.
660 frontend / 504 Python green; version 3.6.29; sw simple-v302.
The RAM explorer's ramFmtPrivileges used a private table with wrong bits and
invented names ("Cum Deletion Ctr", "GSM Card Binding", "Global PIN",
"RFM"/"CFM") and no case for 0x80: a live ISD 9E printed four wrong names
and dropped Security Domain. Per GP Card Spec v2.3.1 11.1.2 Tables
11-7/8/9, 9E = Security Domain, Card Lock, Card Terminate, Card Reset, CVM
Management.
- ramFmtPrivileges now delegates to decodePrivileges (one spec table).
- decodePrivileges decodes byte 1's DAP pair per Table 11-7: b8+b7 = DAP
Verification, b8+b7+b1 = Mandated DAP Verification; the flag bits alone
are no privilege.
- The file had two decodePrivileges copies (the runtime used the later one,
the C-APDU parser test the earlier); the duplicate and its private table
are gone - the single implementation plus PRIVILEGE_NAMES live in the
parser section, which the test evaluates (converted to var like PARSE_INS).
- Tests: 9E/9A live values, the DAP pair, byte 2/3 spot checks; the
PRIVILEGE_NAMES spec pin moved to apdu_parse.
659 frontend / 504 Python green; version 3.6.28; sw simple-v301.
The explorer's FF21 block was titled "Memory (GET DATA FF21)" although the
tag is the Extended Card Resources Information (GP tag FF21, TS 102 226
8.2.1.7.2: installed applications, free non-volatile and free volatile
memory), and the detected RAM command format line sat between the Free NV
and Free Volatile lines.
- Heading is now "Card resources (GET DATA FF21)" with a tooltip naming the
tag and its three fields (EN/RU).
- The "RAM command format:" line moved after the resource lines.
- Help EN/RU spells out what FF21 returns; test asserts the format line
follows the resource lines.
657 frontend / 504 Python green; version 3.6.27; sw simple-v300.
GP 11.4.2.1 makes '10' the superset of '20' (Executable Load Files with
their modules), yet the Explore queried '20' plus a separate '10' modules
pass and merged the two. A live card answered 6A86 (incorrect P1-P2) to
'20' while '10' worked (2026-09-28) - all four P1 values "shall be
supported", so this is the guarded-fallback case, not a vendor branch.
- Explore now queries '10' (with module parsing) and falls back to '20'
(no module lists) only when it yields nothing; a successful fallback
drops the primary failure from the result, and the primary error surfaces
only when both fail.
- The separate P1=10 "modules" pass and ramMergeElfData are gone.
656 frontend / 504 Python green; version 3.6.26; sw simple-v299.
The E3 parsers existed since the vendor-trace work; what was missing was
issuing the query. Per GP 11.4.2.2 the compact listing format (P2.b2=0) is
deprecated and a card may reject it, while the expanded form (P2=02 + the
tag list 4F 9F70 C5 C4 CC CE EA) returns the richer registry data.
- ramExpandedQueryApdu(p1): the reference trace bytes
(80F2 <p1> 02 0C 4F005C084F9F70C5C4CCCEEA 00), pinned in a test.
- Explore: when the compact pass for a P1 yields nothing, the query is
retried in the expanded format and the result becomes the listing (the
compact error only surfaces if the retry also fails); the new "Query
expanded registry details (E3)" checkbox runs the expanded pass on top of
a working compact listing and merges the expanded-only fields
(privileges, ELF AID C4, SD AID CC, module lists) into the entries.
- The detected RAM command format (v3.6.24 ram_format) is now shown in the
Explore result + explorer view, the delete verdict and the plain-send
message.
- Tests parse the real trace payloads (NC ISD, ET application, NP ELF E3
templates); help EN/RU updated.
656 frontend / 504 Python green; version 3.6.25; sw simple-v298.
Comparing the new TCA vendor traces (Explore_EP/ET/NC/NP/SE) with our flow
showed why only one card works:
- The PoR capture was gated on SPI2 bit 0x20. The traced cards deliver the
PoR as a proactive SEND SHORT MESSAGE; with our ISD/memory queries
(SPI2=0x01) the fetched TPDU was thrown away - the Explore died with
"no data" and the RAM install steps silently degraded to an unverified
"no_por". The capture is now unconditional (inline ENVELOPE responses and
proactive submits both work), and the STATUS-poll fallback is enabled for
every card.
- `_ram_next_cntr` still wrapped at 32 bits (`% (2 ** 32)`): a 10-digit
counter like 10000AAAC8 came back as 00000AAAC9 after a RAM/Explore step -
the server-side half of the v3.6.23 counter report. It now advances the
full 40 bits.
- RAM command format (TS 102 226 5.2.1: compact C-APDU vs the expanded
`AA` > `22` form) is detected per operation and never stored:
/api/ram-install and /api/ram-install-app always probe, /api/send-ota
probes when the caller passes `ram_format`. The probe is a read-only
GET STATUS [ISD] as the first step (compact, then wrapped) and every
command of the operation uses the detected format. `_wrap_expanded_apdu`
is pinned to the reference bytes `AA0A220880F24000024F0000`; responses
carry `ram_format` and `final_cntr` (accepted probes consume counters, so
the views use the server's next counter). Applet-directed payloads stay
byte-exact; only RAM views (and RAM/GP chain packs) opt in.
504 Python / 653 frontend green; version 3.6.24; sw simple-v297.
spNextCntr coerced the 40-bit replay counter with `>>> 0`, so any counter
above FFFFFFFF lost its top byte after an accepted send: the live
10000AAAC8 came back as 00000AAAC9, and spCntrSyncPreset then stored the
broken value in the card preset - the RAM/Explore flows (ramIncrementCntr,
which is correct) inherited it.
- spNextCntr: 40-bit arithmetic (`% 0x10000000000`, wrap at FFFFFFFFFF
kept) instead of the 32-bit >>> 0 coercion plus slice(-10).
- spCntrAdjust (+/-): the same 40-bit wrap (an 11-digit value could be
produced at the maximum).
- tests: 10000AAAC8 -> 10000AAAC9, separator handling, FFFFFFFFF7 ->
FFFFFFFFF8, FFFFFFFFFFF -> 0000000000 wrap, out-of-range input clamps.
The server side (_increment_counter_hex, _ram_next_cntr, final_cntr) was
already exact. 653 frontend / 500 Python green; version 3.6.23;
sw simple-v296.
The event list tables carried labels like
"12 Reserved for 3GPP - Network rejection (TS 31.111 7.5.2)". The phrase is
an ETSI-to-3GPP document pointer, not a usage restriction, and it is not
what a user needs - the event name is.
- EVENT_NAMES (PWA + server mirror): the 3GPP-assigned codings carry their
plain TS 31.111 7.5 name ((I-)WLAN access status, Network rejection, CSG
cell selection, IMS registration, Incoming IMS data, Data connection
status change, CAG cell selection, Slices status change).
- 0x20-0x22: "Reserved (future usage)" (no attribution).
- 0x0B/0x14: the spec's full "(single access technology)" / "(multiple
access technologies)" wording instead of the cryptic "(single)/(multiple)".
- tests pin the exact names and that no name contains the cross-reference.
652 frontend / 500 Python green; version 3.6.22; sw simple-v295.
The plain "Send to Card" path refreshed the whole preset right before
sending (spRefreshFromPreset -> cardsApply -> cardsApplyFields) and
re-applied the preset's slot TAR - so a TAR typed for a push/link trigger,
an applet OTA message or the HTTP OTA admin params was silently replaced by
the preset's ISD TAR (the pack buttons for those sources deliberately leave
the TAR manual; the help even said so).
- spRefreshFromPreset(selId, keepTar): the plain send passes keepTar, so
the form's TAR survives; the counter, keys and SPI are still re-read.
- selecting a card preset, ramApplyCard and the chain packs still set the
TAR; the RAM operations keep the preset's ISD TAR by design.
- spParams()/getRamSpParams() normalise the TAR (strip/upper/pad to 6);
only the preview padded before, a short TAR went to the server raw.
- help EN/RU clarified, cards_counter/ram tests updated, sw simple-v294.
652 frontend / 499 Python green; version 3.6.21.
The v3.6.17 prefix built `C9` only when its field was filled, so filling the
C7/C8 quotas with an empty C9 produced `EF…`-only Install Parameters - and
GP Table 11-49 makes C9 mandatory, so the card rejected the data field with
6A80 (decrypted live packet: params started `EF08C7020000C8020000EA25…`).
- `ramParamsPrefix` always emits the C9 TLV (empty -> `C900`, like the
reference tools); the vendor form becomes `C900 EF08 C7020000 C8020000
EA…`.
- the raw "Install parameters (hex)" field is relabelled "Extra install
parameters (hex)" - it appends extra TLVs after the generated prefix.
- tests updated (empty prefix -> `C900`, quotas-only ->
`C900EF08C7020000C8020000`).
650 frontend / 499 Python green; version 3.6.20; sw simple-v293.
`_ram_send_gp_apdu` (the shared step sender of /api/ram-install and
/api/ram-install-app) now logs every step's plaintext APDU and packed
packet, like /api/send-ota's unconditional lines:
RAM C-APDU (INSTALL [for load]): 80E602000C07F0414C4641610100000000
RAM SECURED-PACKET (INSTALL [for load]): 00281516212525000000...
No flag - the step name keeps the sequence readable, and --apdu-trace stays
the card-level (ENVELOPE) trace. Tests: the logging pinned with stubbed
packet builders.
498 Python / 650 frontend green; version 3.6.19; sw simple-v292.
Iterating the final install step no longer needs a delete + re-load of the
.cap:
- `POST /api/ram-install-app` runs INSTALL [for install] (`mode: "install"`,
P1 0x0C/0x04 by make_selectable) or INSTALL [for make selectable]
(`mode: "make_selectable"`, GP 11.5.2.3.3 Table 11-44) for an already
loaded package. The APDU builders are shared with `_cap_apdu_sequence`
(`_cap_install_apdu`, `_cap_make_selectable_apdu` - one builder, no drift)
and the per-step send/PoR/counter logic is the extracted shared
`_ram_send_gp_apdu` (used by /api/ram-install too).
- PWA: the RAM op selector gains "INSTALL [for install] (loaded package)"
and "INSTALL [for make selectable]" with Load File / Module / Instance AID
inputs (instance defaults to the module AID); the result renders through
the shared `ramShowInstallResult` and `final_cntr` keeps the preset in
sync.
- Explore: an **Install** button on a package's module AIDs that have no
instance yet, and a **Make selectable** button on INSTALLED (0x03)
instances - both switch the op and pre-fill the AIDs.
- tests: the install APDU pinned to the live no-STK packet and to the
sequence tail, the make-selectable APDU, ramHasInstance, the Explore
buttons and the op wiring; docs/api.md updated.
650 frontend / 498 Python green; version 3.6.18; sw simple-v291.
The vendor's working install script (samples/uicc/applets/STK3) sends
`C9 22 <config> EF 08 C7 02 0000 C8 02 0000 EA 26 80 0A ... 81 18
<access>` - an applet config in C9 and memory quotas in EF. The RAM install
form now composes that prefix:
- "Applet-specific parameters (C9, hex)" -> `C9 <len> <value>`;
- "C7 / C8 memory quotas (bytes)" -> `EF <len> C7/C8 <2-byte int>` (4-byte
above 32767, GP 9.7);
- the prefix is prepended to the raw "Install parameters (hex)" field and
the server appends the STK part, giving `C9 EF EA`;
- `ramParamsPrefix`/`ramQuotaValue` are pure and tested (incl. the vendor's
`EF 08 C7 02 0000 C8 02 0000` form).
Also: `updateStkParamsHex` shows a hint naming the rejection reason (TAR not
a multiple of 3 bytes / menu ID > 7F / ADF AID not 5-16 bytes) instead of
silently emptying the field, and the UICC_SPECS.md install row notes that
`CA` sits inside `EF` and `CA`+`EA` together return 6A80.
647 frontend / 496 Python green; version 3.6.17; sw simple-v290.
- the ADF AID field gains a "From card" button: it reads the ADF.USIM FCI
via /api/select and extracts the DF name (tag 84) with the new
adfAidFromFci() - the AID is card-specific (the live card's is
A0000000871002FF49FF0589, 12 bytes; the base A0000000871002 is only a
prefix, and a shorter AID makes the card reject the access entry with
6A80).
- the priority field defaults to 255 (0xFF) and is labelled "(0 = RFU)":
TS 102 226 8.2.1.3.2.6 makes '00' RFU, which can break the toolkit
registration; the builder defaults an unset priority to 0xFF and the
chain rows follow.
- stkParamsBuild's formatted payload fields are uppercase now (the rest of
the tool's hex is; toString(16) emits lowercase).
- tests: adfAidFromFci pinned to the live FCI, the From-card wiring, the
updated form expectations.
643 frontend / 496 Python green; version 3.6.16; sw simple-v289.
The Explore delete showed "Failed: por_ok - remote SW 6700" while the delete
executed: ramDeleteApdu appended the trailing Le, and this card's SCP80
layer counts a trailing byte as a phantom second command whose SW 6700
masked the real result (count=2, last SW 6700) - the same quirk as
INSTALL [for load] before v3.6.8.
- ramDeleteApdu and the chain builder's DELETE row emit the case-3 APDU
(4F AID TLV, no Le); the SCP81 Delete-AID template follows (it reuses
ramDeleteApdu).
- tests updated (delete APDU expectations in ram/ram_counter_flow/scripts).
641 frontend / 496 Python green; version 3.6.15; sw simple-v288.
TS 102 226 8.2.1.3.2.2 defines the EA inner tags as 80 (toolkit params),
C3 (toolkit DAP), 81 (UICC Access Application specific parameters) and 82
(UICC *Administrative* Access) - the plain-text spec extract had shifted the
table columns, so the access list was emitted under 82. With the list under
82 the card rejected the ADF.USIM entry with 6A80 and the applet never got
its uicc.access.FileView rights (live 2026-09-28).
- stkParamsBuild emits the access list under 81 (the structure is unchanged:
the admin field is coded the same way per 8.2.1.3.2.2.4).
- tests updated (81 04 / 81 0F / 81 17 expectations).
641 frontend / 496 Python green; version 3.6.14; sw simple-v287.
"Failed: 9000" although the delete executed: the RAM helpers sent
`sp-spi2` - the base SPI2 select, whose default is "00 - No PoR" - so the
card answered the envelope 9000 with no PoR and the verdict treated the
missing PoR as a failure. `cardsApplyFields` also set `sp-spi2-hex` from
the preset and then `updateSp()` recomputed it from the selects (which the
preset never updated), clobbering the preset byte for anything reading it.
- `getRamSpParams()` reads the computed `sp-spi2-hex`; `cardsApplyFields`
syncs the SPI2 selects from the preset byte (base = low 5 bits, bit 0x20
= PoR via SMS-SUBMIT, dynamic option for unlisted bases) and the base
select gains the missing cipher + RC/CC/DS combinations (15/19/1D).
- a missing PoR is no longer a failure (the SPI may request none, and the
card sometimes refuses one): `OK` with the SW, or `OK (no PoR)`.
- a successful delete drops the object from the Explore result locally (no
re-explore): an applet row goes away; a cascade package delete also drops
the applets whose AID starts with the package AID. The consumed counter
is still saved.
- tests: ramRemoveFromExplorer (app/cascade/prefix), the no-PoR flow, the
SPI2 source guard, and the updated delete-flow stubs.
641 frontend / 496 Python green; version 3.6.13; sw simple-v286.
Live findings (2026-09-28): a browser-restored Receipt Generation privilege
bit (third byte, ISD-only per GP Table 6-2) was silently sent and produced
6985, and the '82' access entries were missing the mandatory "Length of
Access Domain DAP" byte, so the card rejected the ADF.USIM entry with 6A80.
- ramResetGrants() clears the privilege / toolkit-enable / file-access
checkboxes and refreshes the aggregates on load; ramInstallCap re-derives
the privileges from the checkboxes at send time - no stale or
browser-restored grant can be sent.
- the privileges aggregate emits 1 or 3 bytes, never the invalid 2-byte
form (GP Table 11-43), in both updateRcPriv and the chain's computePriv.
- '82' entries carry the DAP-length byte: '00 01 00 00' (shared FS) and
'<len> <ADF AID> 01 00 00' (ADF); the ADF AID is editable
(rc-tk-adfaid, default A0000000871002 = ADF.USIM, 5..16 bytes enforced).
- tests: ram_grants.test.js (aggregate forms, the send-time derivation, the
load-time reset) and the updated access-parameter shapes in
stk_params.test.js.
638 frontend / 496 Python green; version 3.6.12; sw simple-v285.
SIM toolkit applets installed while UICC-library applets failed at INSTALL
[for install] with 6F00 - including with the reference tool's exact install
parameters. The asymmetry: the SIM (CA) path grants file access via the
Access Domain field (default 00 = full access), while the UICC (EA) path
sent no '82' (UICC Access Application specific parameters) at all, and the
reference's '82 00' is empty. An applet importing uicc.access (the failing
CAP has 2 refs) can then fail inside its install().
- EA mode gains two checkboxes (RAM install form + the chain rows' toolkit
block): "File system access (full)" appends '82 03 00 01 00' (shared file
system + Access Domain Parameter 00 = full access, TS 102 226
8.2.1.3.2.2.2/8.2.1.3.2.5); "ADF.USIM access (full)" adds
'07 A0000000871002 01 00' to it. Both default off.
- tests: the access TLV shapes (with/without the ADF entry), the form -> hex
path with the checkbox, and the toolkit-field wiring count (16 fields).
635 frontend / 496 Python green; version 3.6.11; sw simple-v284.
The Apps query (P1=40) was sent with SPI2=0x01 while only the ELF queries
(P1=20/10) used SPI2=0x21. The Apps listing can exceed the ENVELOPE
response, and the card then answers the envelope PoR with
`actual_response_sms_submit` - the actual response would follow as an
SMS-SUBMIT, which is never sent unless the PoR is requested in submit mode.
The paginate only accepted `por_ok`, so the Explore reported the raw status
as an error ("Partial - Apps: actual_response_sms_submit") and the Apps list
never rendered (live 2026-09-28).
- `ramListingSpi2(p1)`: 40/20/10 -> '21', else '01'; the paginate uses it.
- the paginate accepts `actual_response_sms_submit` via `spPorAccepted` and,
when no data arrived, notes "response via SMS not captured" instead of
reporting the card status as a failure.
- the Explore delete verdict follows the same acceptance rule (an accepted
SMS-submit delete no longer shows "Failed" while its counter advanced) and
no longer prints a dangling " -> " without a SW.
- tests: ramListingSpi2 + the delete-accepted-via-sms-submit flow.
633 frontend / 496 Python green; version 3.6.10; sw simple-v283.
The RAM install form sent the `STK parameters (hex)` field, which was only
regenerated when the toolkit checkbox or the mode select changed - editing
the TAR (or any other toolkit field) afterwards was silently dropped. The
live install therefore carried the form defaults (TAR B00001, textLen 0,
menus 0, MSL 16, channels 0) instead of the entered AF4D01 / MSL 12 /
channels 1, and the card rejected the install parameters with 6A80
(TS 102 226 8.2.1.3.2.7: a TAR already assigned on the card).
- every `rc-tk-*` field regenerates the hex on input/change; the install
recomputes it at send time unless the user hand-edited the hex
(`dataset.manual`), so the edit order can no longer drop values;
- one pure `stkParamsBuild` serves the RAM form (`buildRcToolkitParams`) and
the RAM/GP chain rows (`buildTkParams`) - no drift between them (the chain
row also gains the menu-ID <= 7F check);
- applet TAR field: empty by default, no placeholder - B0 00 01 is the
allocated ADF RFM TAR (TS 101 220 Annex D), not an applet TAR; an empty
field is coded as TAR length 00 (the card may take a TAR from the AID only
when the AID carries one, PIX hex digits 15-20);
- the chain builder's INSTALL/LOAD rows drop the trailing Le (the v3.6.8
server form; a trailing Le made the card execute a phantom command);
- tests: stk_params.test.js (the decrypted live parameters, empty-TAR
coding, CA wrapper, long-form lengths, rejection cases, form wiring, and
the form -> hex path with the real builders) and ram_counter_flow.test.js
(the Explore delete persists the consumed counter through the real
ramSaveCntr); STK fixtures no longer use B00001.
631 frontend / 496 Python green; version 3.6.9; sw simple-v282.
INSTALL [for load]/LOAD/INSTALL [for install] carried a trailing Le and an
explicit ISD AID in the Security Domain field; the card executed the extra
byte as a second (phantom) command, so the compact response reported count=2
with SW 6700 - which the v3.6.2 remote-SW check correctly treated as a
failure, aborting at step 1.
Decrypted from the live trace (KIc/KID 25/25, 3DES): ours was
80E6020014 07F0414C46416101 08A000000003000000 00 00 00 00
the reference tool sends
80E602000C 07<aid> 00 00 00 00 (empty SD, no Le)
and its response is count=1 / 9000 while ours was count=2 / 6700.
- `_cap_apdu_sequence`: the SD AID is only sent when a custom one was
supplied (empty -> '00', the card defaults to the ISD; GP 11.5.2.3.1
Table 11-42) and all three RAM install APDUs are case 3 (no Le).
- tests: the expected INSTALL bytes updated, a no-Le assertion for every
APDU in the sequence, and a custom-SD-AID case.
615 frontend / 496 Python green; version 3.6.8; sw simple-v281.
Delete All worked but reset the preset counter: the handler never re-read
the preset (unlike ramExecute), saved N+1, then re-ran the Explore with the
OLD sp (counter N). Every page answered cntr_low, nothing was accepted, and
ramExplore's final ramSaveCntr wrote N back over the preset.
- ramDeleteFromExplorer re-reads the preset first (spRefreshFromPreset
'ram-card-sel'), advances and saves the counter only for a packet the card
accepted (spPorAccepted) - a rejected packet leaves it untouched - and
continues the follow-up Explore from the consumed counter instead of
replaying it.
- the delete result also checks the remote command's SW via a new
`ramRemoteSwOk` (9000 / 61xx / 62xx / 63xx / CAFE, mirroring the server's
`_ram_remote_sw_ok`): a por_ok packet whose DELETE was refused (e.g. 6A88)
no longer shows "OK" (the counter still advances, the card consumed it).
- ramExplore only saves the counter when it advanced, so a stale caller can
never lower a preset counter again.
- tests: flow tests for accepted / rejected / refused-remote-SW deletes
(refresh order, saved counter, the counter passed to the re-explore) and
the ramRemoteSwOk set.
615 frontend / 495 Python green; version 3.6.7; sw simple-v280.
ramDeleteFromExplorer() called an undefined `_ber_len()` helper, so clicking
Delete / Delete All raised a ReferenceError right after the confirm and no
APDU was ever sent. The inline builder also omitted the mandatory Le byte.
- new pure `ramDeleteApdu(aid, withCascade)` = GP Card Spec v2.3.1 Table
11-20/23 form: `80 E4 00 <p2> <Lc> 4F <len> <AID> 00` (P2 00 = object,
80 = object and related objects), used by the Explore handler.
- `scp81DeleteApdus()` (the "Delete AID" script template) now shares the
same builder: it used to send the raw AID without the mandatory '4F' TLV.
- tests: exact bytes for 7/16-byte AIDs and both P2 modes, the SCP81 script
expectations updated to the TLV form, and a wiring check that the undefined
helper call does not come back (`_ber_len(`).
611 frontend / 495 Python green; version 3.6.6; sw simple-v279.
Explore still missed the F0414C46416101 package on a card whose responses
wrap the R-APDU in the TS 102 226 5.2.2 Response Scripting template
(`AB <len> 80 <count> 23 <len> <R-APDU>`): `_decode_por` parsed that as a
compact response, so the frontend got `last_status_word` 81d0/7680 and data
starting `80 01 01 23 ...` instead of the listing.
- server: `_parse_response_scripting()` (AB definite / AF 80 ... 00 00
indefinite) extracts the executed-command count and the last R-APDU's SW
and data; `_decode_por` exposes it as `response_type: scripting` in the
same `decoded` shape as compact, so the RAM explore paging and the RAM
install `por_sw` see the real 9000/6310.
- frontend: the compact listing walk is deterministic on the AID length
(`len AID life ver`; P1=10 adds `module_count (len module_AID)*`).
`_parseRawAppEntry` no longer guesses `rawLen-1` for >8-byte AIDs (16-byte
A113 applet AIDs were truncated), and `_parseRawElfEntry` no longer scans
for `0x10` (a length/AID byte equal to 0x10 derailed the walk: a live page
parsed to 1 entry with no F0414C46416101).
- tests: exact trace fixtures - the ELF page lists F0414C46416101 (11
entries), the P1=10 page attaches its F0414C4641610101 module, the app page
keeps the 16-byte A113 AIDs; Python covers the scripting template
(definite/indefinite) against the live `AB 12` ISD and listing vectors.
610 frontend / 495 Python green; version 3.6.5; sw simple-v278.
Explore returned partial data (no ELF/module entries, the installed package
missing) and then failed with cntr_low. Two causes, both in the
actual-response SMS-SUBMIT path the card uses for big listings:
- `_find_sms_tpdu` read TLV lengths as a single byte; the FETCH carries
`8B 81 97 ...` (BER long form) for the big pages, so it returned a
corrupted, truncated TPDU.
- `_calc_ud_offset` treated the SMS-SUBMIT relative validity period (VPF=10)
as 7 bytes instead of 1, shifting the UD offset: `_parse_sms_concat` then
read a bogus UDH and reported no concatenation, so the segments never
assembled and the PoR/data was dropped ("RAM RESPONSE-PACKET: empty").
The step was marked failed, the counter did not advance, the same counter
was retried and the card answered `cntr_low`, which also blocked P1=10
(modules) where the installed package appears.
- the captured segments now always store the assembled UD (`submit_ud_hex`);
`_sms_submit_por()` rebuilds the DELIVER-style packet (`02 71 00` + UD) for
`_decode_por`. Verified against the live capture: por_ok, remote SW 6310,
438 hex chars of listing data (the exact bytes of the P1=20 page).
- `spPorAccepted` counts `actual_response_sms_submit` (0x0B) as accepted, so
the counter advances when the data follows via SMS-SUBMIT.
Explore queries follow GP Card Spec v2.3.1 11.4.2.2: compact listings
(P2.b2=0) with the chained GET RESPONSE (`ramGetStatusApdu`, paging P2=00 ->
P2=01), the malformed P2=02 attempt is gone, and the ISD-only query (P1=80)
never pages with next-occurrence (the card shall reject it).
Tests: Python SmsSubmitCaptureTest with the live FETCH bytes (TPDU length,
UD offset, concat reassembly, 027100+UD decode); ram.test.js checks the APDU
builder and the missing P2=02 attempt; cards_counter covers 0x0B.
608 frontend / 492 Python green; version 3.6.4; sw simple-v277.
The SCP80/RAM forms hold a copy of the preset (counter, keys, TAR, SPI).
Editing the preset on the Cards tab saved correctly, but the form kept the
old copy: the operation sent the stale counter (the card answers cntr_low)
and the post-send sync wrote the stale value back over the preset - the
saved counter silently reverted. Reproduced in a real DOM:
after select in SCP80: preset=0000000001 sp=0000000001
after Cards edit+save: preset=00000000AA sp=0000000001
after a sync: preset=0000000001 (edit lost)
- `cardsApply()` split into `cardsApplyFields()` (field copy, no packet) and
`cardsApply()` = fields + genSp; new `spRefreshFromPreset(selId)` re-reads
the selected preset from `sp-card-sel` / `ram-card-sel` and re-applies it.
- `pysimSendOta()` and `ramExecute()` call it before starting, so every
SCP80/RAM operation uses the preset as it is now.
- A rejected send no longer advances the counter: new `spPorAccepted(por)`
gates the advance+write-back in `pysimSendOta`, the Explore pagination and
its GET DATA step, and the server's RAM install (`_ram_next_cntr`: advance
only for `por_ok`/`no_por` steps). A failed install still returns
`final_cntr` (the accepted prefix) and the PWA persists it, so a retry
never replays a counter the card already consumed.
- tests: cards_counter.test.js (the stale-form regression, RAM selector,
fields-without-genSp, spPorAccepted) and `_ram_next_cntr` cases; the
cards_form/ram harnesses updated for the split.
- docs/api.md counter semantics; AGENTS preset-source-of-truth rule.
607 frontend / 488 Python green; version 3.6.3; sw simple-v276.
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.
#phone-sub-esim was missing its closing </div> before #phone-sub-test, so
the browser parsed the test panel (and everything after) as a child of the
eSIM panel. Clicking Test script unhid the panel itself, but its hidden
ancestor kept it invisible - the pill has never shown a panel in a real
browser since it was added (v3.5.11). The eSIM LPA pill conversely showed
the whole test editor below its own fields.
The old whole-file div-count test stayed green because the inline script's
template strings happen to contain one extra </div>; the real markup was
409 opens vs 408 closes.
- add the missing </div>.
- html.test.js: count divs on the markup only (scripts excluded) and add a
structural walk (stray end tags, cross-nesting, unclosed elements, HTML
optional end tags honoured) over index.html, help.html and help-ru.html,
plus a Simulator-panel sibling-depth check. Run this for every HTML edit.
- the walk immediately found the same class of bug in both help pages: six
unclosed <section> chapters each; closed them.
- AGENTS: the HTML structure test is now part of the working conventions.
602 frontend / 482 Python green; version 3.6.1; sw simple-v274.
BIP was only reachable through the SCP81 listener; cards that use TCP for
other purposes (an applet's OPEN CHANNEL, a push trigger without HTTP OTA)
now get a generic control surface, independent of SCP81.
Server:
- /api/bip/control starts a plain BIP session in three modes: sink
(default; a local TcpDumpServer that accepts the card's channels and only
logs conn/sink-rx/conn-close, never answering; port 0 = ephemeral, the
bound port is reported), passthru (dial the OPEN CHANNEL destination, TCP
client only) and redirect (fixed host:port).
- /api/bip/status returns {owner, bip, listener}; /api/bip/log and
/api/bip/log-clear share the BIP event log with /api/scp81/log.
- The session state is shared with the SCP81 listener and only one session
runs at a time: _bip_session_stop() stops whichever control started it
and the control responses report it as `replaced` (both directions,
SCP81 <-> BIP). State renamed _SCP81_MODE/_TARGET/_LISTENER/_LINK_EVENTS
-> _BIP_* plus _BIP_OWNER.
- TcpDumpServer logs conn-close and counts accepted connections; stop()
joins the accept thread so the port is really free on restart.
- The new control endpoints are blocked during test-script runs.
PWA:
- New Simulator pill BIP (after TR Config): mode select with notes,
Host/Port rules, Start/Stop with a "replaced" notice, status line (mode,
bound address, owner, channels), a Channels box and the shared BIP log
(reuses the SCP81 log renderer, now showing `peer`).
- The SCP81 status line marks a session owned by the BIP pill.
Help EN/RU 8.10, docs/api.md, AGENTS; CAT_TP/UDP recorded as not
implemented.
Tests: tests/test_bip.py (9) and frontend/tests/bip.test.js (6).
600 frontend / 482 Python green; version 3.6.0; sw simple-v273.
The CAP analysis now lives in the simulator-style fieldset with the title
embedded in the border:
<fieldset id="ram-cap-info" class="hidden mt-2 border ... p-3 text-xs ...">
<legend ... data-l10n="CAP details">CAP details</legend>
<div id="ram-cap-body"></div>
</fieldset>
- Both CAP boxes (RAM installer, SCP81 Scripts) use the same bordered
fieldset + legend pattern as the simulator/eSIM/card fieldsets.
- capRenderAnalysis toggles the fieldset and writes into the body div, so
the border and title stay visible for analyzing / failed (Retry) / done
states alike.
- capMemHtml is pure content now; the inner "CAP requirements (estimate)"
heading is gone - the border legend titles the box.
- i18n: 'CAP details' -> "Детали CAP" (static legend, data-l10n).
- refreshDynamicI18n() rebuilds both CAP bodies on a language switch (the
dynamic report previously kept the old language).
Tests: the wiring test requires both legends and body divs, a new
capRenderAnalysis test covers idle/analyzing/error/ok, and the renderer
tests no longer expect the inner heading.
593 frontend / 473 Python green; version 3.5.19; sw simple-v272.
The imported-library AIDs are now shown in a dedicated column between the
library name and the minimum version (Library | AID | Version | Family |
Refs), monospace and break-all so the 32-char UICC/3GPP AIDs wrap cleanly
on narrow panels. When a library name cannot be resolved (vendor AID)
the name cell shows a dash instead of repeating the AID now carried by
the column.
Tests updated: the Requires slice asserts the AID header and every row's
AID, and the unresolved-name case expects the dash plus the AID column.
Help EN/RU reworded; the AID key is a no-op in RU.
592 frontend / 473 Python green; version 3.5.18; sw simple-v271.
The CAP analysis box was a flat list mixing memory, package and library
data, with run-on "table-like" lines of differing lengths. Rebuild it as
a labelled report (always visible except the bulky parts, which stay in
nested disclosures):
- Package: AID (plus the optional JC 2.2 package_name), version, header
flags (applet/exports/int), applet AIDs and the compiled-against hint
(javacard.framework -> Java Card SDK release + CAP format).
- Requires (n): one row per imported library with the minimum version
(">=", same major and minor >= the recorded export version per JC VM
4.5.2), the API family and the distinct constant-pool reference count.
The AID is not repeated when the name resolves (redundant); unresolved
AIDs are shown as the name.
- Memory: the NVRAM requirement stated once, with indented children (code
image + persistent data parts), RAM (volatile) and the suggested
C6/C7/C8 quotas - the old duplicated totals are gone.
- Components: collapsed table in load-file order with size and share,
ending in the load-file total row; Notes holds the GP caveat and the
estimate disclaimer.
- Numbers line up right-aligned in monospace columns; section labels use
uppercase letter-spacing inline, so no Tailwind rebuild is needed.
- i18n: 27 new EN/RU keys; help EN/RU and AGENTS updated.
Frontend tests reworked for the new markup (group labels, aligned values,
load-file order, total row, and the no-imports / unknown-AID /
older-response / missing-components cases).
592 frontend / 473 Python green; version 3.5.17; sw simple-v270.
The Import component (JC VM spec 6.6) is now exposed by /api/cap-info and
rendered in the CAP analysis box:
- imports: the libraries the CAP is linked against with the export-file
versions and the number of distinct constant-pool references (6.7),
displayed as "name >= version" (a card resolves an import only with the
same major and a minor >= the recorded one, 4.5.2). Standard names come
from the AID table; each gets a family label (Oracle JavaCard / ETSI SIM
2G / ETSI UICC / 3GPP USIM-ISIM / GlobalPlatform) and, for
javacard.framework, a Java Card SDK release hint derived from the local
Oracle SDK kit corpus (jc211..jc305u4 exports; unknown versions stay
unhinted). Vendor/applet AIDs stay bare.
- Header package flags (Table 6-4: int / exports / applet package) and the
optional JC 2.2 package_name (absent in all CAP 2.1 files).
- components: every archive entry in load-file order with its size and
share of the load file (including the Directory/Export entries capmem
does not parse); the sizes sum to load_file_bytes.
- The PWA box leads with "Requires: ..." when imports exist, keeps the
compiled-against line (Java Card hint + CAP format), the package/applet
identity, the import details (family, refs, AID) and the component
breakdown in Details; a memory-only response still renders.
Tests: Python +2 (imports/flags/name/components; header flags + package
name) with the synthetic CAP builder extended; frontend +2 renderer cases
(unknown AIDs, no-import responses) plus jcAidNorm/jcAidFamily tests; the
jcAidNorm extraction added to the ram/scp81 harnesses.
Help EN/RU, docs/api.md, AGENTS.
591 frontend / 473 Python green; version 3.5.16; sw simple-v269.
GlobalPlatform models this explicitly (GP Card Spec v2.3.1 Table 11-48,
load parameters): C6 = non-volatile code, C7 = volatile data, C8 =
non-volatile data, and 11.5.2.3.7 - when the card makes no code/data
distinction the required minimum is C6 + C8. The analysis now reports it:
- capmem.memory_json() accepts the load file size (all CAP components -
the package image the card stores) and returns code.load_file,
nvram.requirement = load_file + persistent data, and sets the suggested
C6 to the load file (the bytecode-only Method.cap figure stays in
code.method_component); _cap_info_body passes the size it already
computed.
- The CAP estimate box (both the RAM installer and the SCP81
Install-from-.cap form) leads with "NVRAM requirement ≈ code image +
data" plus the RAM estimate, keeps the bytecode/other-component split and
the full data breakdown in Details, and carries the GP citation with the
caveats (card memory management, allocation rounding and the registry
entry are not included; the static field image appears in both parts).
- An older server response without code.load_file still renders (the
bytecode size is used as the fallback).
Tests: Python +1 (load-file semantics) with extended cap-info assertions,
frontend +1 (renderer + fallback). Help EN/RU, docs/api.md, AGENTS.
587 frontend / 472 Python green; version 3.5.15; sw simple-v268.
- The SCP80 counter write-back resolves the preset by NAME first (the run
snapshot prefers the name) and only falls back to an ICCID-looking
value: presets with digits in their names are found after a page reload
(the previous fallback ran cardsNormIccid on the name and gave up).
- testScriptProblem validates the *selected* SCP80 source (like the form
and the server), so source=apdu with only sp filled is caught locally
instead of failing with a server 400.
- _test_run_start cleans up (_TEST_RUNNING=False, run state error) when
the worker thread cannot be created/started, and the endpoint answers
500 with a clear error instead of leaving the card blocked behind a run
that never started.
- The 5 s poll writes the counter back whenever a run is finished
(idempotent), so a reloaded page saves it without visiting the pill.
- Mask wildcards ('?') are stripped from the hex fields that cannot carry
a mask (APDU, secured packet, event/file data, TAR/SPI overrides, DCS,
extra TLVs); check values keep them.
Tests: frontend +3 (write-back by name / by ICCID, mask-free fields,
source-aware script check), Python +1 (failed thread start unblocks).
586 frontend / 471 Python green; version 3.5.14; sw simple-v267.
PWA:
- The SCP80 "Source" switch sticks: the choice is stored in
`params.source`, switching keeps both values (the server honors the
explicit source when both are present, and the form validates only the
selected one).
- The SW check field is empty by default (placeholder "default: 9000
(91?? when polling)"), so the server defaults apply - previously the
pre-filled 9000 defeated the polling STATUS default and a card that
announced a command made the step fail with "expected 9000".
- Item text checks offer contains/exact only (the mask mode was rejected
by the server validation).
- The SCP80 counter is written back to the preset even when the run is
observed after a page reload or was started elsewhere (resolved by
ICCID or preset name from the run snapshot).
Server:
- Step-entry mutations happen under `_TEST_LOCK`
(`_test_entry_update`/`_test_finish_entry`): the status endpoint
serializes the state with json.dumps, so entries must not change while
it iterates them.
- The pending-command drains (unexpected command, error, stop) hold
`_CARD_LOCK` like every other card conversation.
- `_int` accepts plain decimals with leading zeros and 0x hex.
- The scripted TERMINAL RESPONSE text string uses the CR-set tag `8D`
(consistent with the other TR TLVs; both are legal).
- A script-driven menu selection mirrors `server.menu_active`.
Tests: frontend +4 (source switch/round-trip, status SW default, render
checks, item modes), Python +3 (integer parsing, menu_active, source
selection). Help/docs unaffected beyond api.md's `source` note.
583 frontend / 470 Python green.
- Top-level tab label "Phone simulator" -> "Simulator" (RU "Симулятор");
the DOM ids stay tab-phone / phone-sub-*.
- The eSIM pill inside the Simulator tab is now "eSIM LPA" (EN/RU),
matching the section's ES10/LPA scope; the panel id stays phone-sub-esim.
- Documentation updated: help EN/RU (section 8 Simulator, list of the four
pills including Test script, tab lists and cross-references),
README/README_RUS (tab lists, Simulator section, eSIM LPA), AGENTS
repo/root.
- Test titles updated; version 3.5.12; sw cache simple-v265.
579 frontend / 467 Python green.
- Phone simulator tab: a fourth pill **Test script** - script list
(localStorage `simple_tests`) with New/Clone/Delete/Export/Import, a
form-based step editor (modal) and a run panel.
- Actions: ENVELOPE (Event Download), Menu Selection, UPDATE/READ file by
path, raw APDU, SCP80 (C-APDU or a pre-built packet, per-step TAR/SPI1/
SPI2 overrides; the preset is matched by the equipped card's ICCID and
the final counter is written back) and STATUS with an optional poll.
Every action has SW/data checks (exact or ?-mask) and a PoR check for
SCP80, with an error (stops) / warning (continues) fail level.
- Expectations: FETCH the command announced by the previous step, verify
type/qualifier/text/item/raw and answer with the scripted TERMINAL
RESPONSE (result, item id, text, extra TLVs); templates for STK menu
browsing and an applet via SCP80.
- Run panel: progress banner (plus a marker on the pill), per-step
OK/Warning/Error badges with expected vs actual values, sent/received
hex, decoded commands and the SCP80 counter; 500 ms polling while a run
is active, and a running script disables other card controls
(data-needs gate; the server answers 409 to other card endpoints).
- Help EN/RU (8.9 Test script), AGENTS; version 3.5.11; sw simple-v264.
579 frontend / 467 Python green.
A test script drives a deterministic dialogue with the card: action steps
(ENVELOPE event / Menu Selection, raw APDU, SCP80 secured packet with a
card-preset, file update/read, STATUS) with SW/data/PoR checks, and
proactive-command expectations that fetch, check (command type, qualifier,
text/item/raw) and answer with a scripted TERMINAL RESPONSE.
- pysim_simple_server/testscript.py: pure engine (validation, exact/mask
matchers with '?' nibble wildcards, item/text checks, TERMINAL RESPONSE
building).
- server.py: worker thread + state, /api/test/run|status|stop|clear, the
card-endpoint guard (409 while running), background STATUS polling
suspended, 'error terminates / warning continues', a pending command is
drained with a cancel TR on stop/error, no-drain modes for the ENVELOPE
and SCP80 senders (the pending command belongs to the next expectation).
- Expectations never poll: a command must be pending (91XX) from the
previous step, otherwise it is an error (TS 102 221 7.4.2.1 / TS 102 223
6.3); scripts add an explicit `status` action (attempts/interval) when
the card delivers on poll.
- SCP80 steps require a complete card preset, may override TAR/SPI1/SPI2
only, and the counter is advanced per send and reported
(`scp80_counter`) for the PWA to write back.
- tests/test_testscript.py (26 tests: engine, matchers, TR building, the
STK menu dialogue, error/warning termination, status polling,
unexpected-command drain, SCP80 preset/counter, file actions, guards).
- docs/api.md endpoint reference.
467 Python / 573 frontend green.
- Background STATUS polling is enabled by default, per the spec's idle
polling rule (TS 102 221 14.6.2): `_POLL_ENABLED = True`, __main__ runs
`_poll_enable()` regardless of card presence (--poll-interval 0 still
disables) and `_do_status_poll` keeps ticking while enabled even without
a session, so a cardless start resumes as soon as a card appears.
- A new card session clears a POLLING OFF from the previous card in
`_apply_equipped_card` before re-enabling polling.
- The Phone-tab toggle now shows the *effective* state: OFF in amber
("card disabled polling (POLLING OFF)") while the card suspended
proactive polling, back to ON on the next POLL INTERVAL. The 5 s
background poll now passes `card_disabled` through (it was dropped, so
an autonomously received POLLING OFF never reached the UI).
- Tests: poll_ui updated (button OFF while suspended, warning path),
test_poll +2 (cardless ticking, module default via a subprocess check).
- Help EN/RU, docs/api.md, AGENTS; version 3.5.10; sw cache simple-v263.
573 frontend / 441 Python green.
- EVENT_NAMES (server + PWA) corrected against TS 102 223 v18.3.0 8.25:
0x14 is "Access technology change (multiple)" (not Change of UICC
Access), 0x19 Profile container, 0x1A Void, 0x1B Secured profile
container, 0x1C Poll interval negotiation, 0x20-0x22 reserved. Values
the CAT spec leaves "Reserved for 3GPP" now carry the concrete TS 31.111
event name + clause (0x11 (I-)WLAN access status, 0x12 Network
rejection, 0x15 CSG cell selection, 0x17 IMS registration, 0x18 Incoming
IMS data, 0x1D Data connection status change, 0x1E CAG cell selection,
0x1F Slices status change).
- Poll Interval Negotiation (0x1C) in the Phone tab: the form proposes a
Duration (unit + interval) and the UICC's answer (TS 102 223 8.97
accepted / rejected / modified + optional Duration) is decoded and
shown; a "modified" duration becomes the background poll interval. The
form appears when the card subscribed to the event, like every other.
- Polling emulation is card-driven: a POLL INTERVAL adopts its Duration
(minutes/seconds/tenths -> 1..255 s, logged) for the background poll,
is echoed in the TERMINAL RESPONSE (6.8.4) and clears a POLLING OFF
suspension; POLLING OFF (6.4.14) suspends proactive polling until a new
POLL INTERVAL - the manual Send STATUS button and presence detection are
unaffected. /api/poll-status and /api/poll-toggle report card_disabled
(+ a warning when enabling while suspended); the PWA shows it in amber.
- Tests: tests/test_poll.py +7, test_proactive_names.py +3, frontend
event_forms +2 and poll_ui (4). Help EN/RU, docs/api.md, AGENTS.
573 frontend / 439 Python green; version 3.5.9; sw cache simple-v262.
Selecting a .cap in the RAM installer or the SCP81 "Install from .cap"
template now runs a read-only analysis (POST /api/cap-info) before any
APDU is built: the archive is validated structurally (a corrupt or
wrong-format file fails here) and the bundled capmem analyzer estimates
the code size and the persistent (NVRAM) / volatile (RAM) requirements,
with the tool's suggested C6/C7/C8 quotas shown as information. The
form's action button (Execute / Generate) stays disabled until the
analysis succeeds - pressing it is the user's confirmation to continue.
- pysim_simple_server/capmem.py: bundled analyzer (component parsers +
JCVM opcode table + method-bytecode allocation scan), adapted to take
the CAP archive as bytes and return report/memory dicts; output
verified byte-identical to the workspace tool on 21 real CAPs.
- _cap_info_body + POST /api/cap-info (read-only; the install endpoints
stay unchanged and self-sufficient).
- PWA: shared capAnalyzeFile/capMemHtml/capGateOk helpers, estimate box
under both CAP inputs (reusing the idle #ram-cap-info div, new
#scripts-cap-info), data-cap-gate gating in pysimApplyAvailability,
stale-response guard, Retry, EN/RU strings.
- Tests: tests/test_cap_memory.py (synthetic CAPs: new/newarray/
makeTransientByteArray/static fields/unknown-opcode warnings/corrupt
input), frontend capmem.test.js (renderer, gate, analyze flow).
- help EN/RU, docs/api.md, AGENTS; version 3.5.8; sw cache simple-v261.
567 frontend / 429 Python green.
The RAM Explore view, the SCP81 GET STATUS script results and the R-APDU
parser tree showed package AIDs as bare hex. A shared resolver now
annotates the known standard JavaCard / ETSI / 3GPP / GlobalPlatform
package AIDs with their library name (workspace export-file study
`docs/JAVACARD.md`, 2026-09-26, plus the GP default ISD AID from GP Card
Spec v2.3.1 H.1.3); a RID table gives a weak owner hint for otherwise
unknown AIDs, and vendor/applet AIDs stay bare.
- JC_AID_NAMES / JC_AID_RIDS + jcAidName / jcAidSuffix / jcAidHtml.
- Wired into ramRenderExploreHtml (ISD, applications, ELFs, module and SD
AIDs), scp81ResultLines GET STATUS listings and decodeTlvValue
(4F/84/C4/CC - the R-APDU parser tree).
- aid_names.test.js (families, normalisation, RID hints, malformed input,
table sanity) plus render assertions in ram.test.js and scp81.test.js.
- Help EN/RU note the annotation; table is hand-maintained from the note.
561 frontend / 421 Python green; version 3.5.7; sw cache simple-v260.
The STK menu overlay's cached top menu showed the card-provided Items Next
Action Indicator (TS 102 223 8.24) as a gray suffix, but the pending
SELECT ITEM list the card returns mid-dialogue did not: the server already
attaches nai_name (via _parse_select_item), the panel just had its own row
markup without the suffix.
Both lists now render through stkMenuItemsHtml(items, handler) - identical
rows (id, text, optional gray suffix) with the per-list click callback
(stkMenuItemClick / stkSubItemClick).
stk_menu.test.js: the shared renderer (suffix only with nai_name, handler
in the row, null list) plus a call-site guard for both lists.
554 frontend / 421 Python green; version 3.5.6; sw cache simple-v259.