- 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).
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.
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.