Commit Graph

153 Commits

Author SHA1 Message Date
catarrh 391d4a322d Low-counter reporting, counter sync probe, Bad-format decode, expanded forms
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.
2026-10-02 00:17:10 +03:00
catarrh 69abbe2fa5 feat: the conditional event objects are complete
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.
2026-10-01 22:57:01 +03:00
catarrh 7061f42781 feat: the remaining spec-defined event forms (data/display/search/frames, IMS/CAG/slices)
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.
2026-10-01 22:30:19 +03:00
catarrh a26a2b6648 feat: the remaining six event types get their parameter forms
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.
2026-10-01 22:19:55 +03:00
catarrh 994ee6d2a3 ui: the Data Connection event gains the date-time object; the "more" flag is precise
- 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.
2026-10-01 22:07:24 +03:00
catarrh 0cccd62230 release: v3.9.0 - the operation start buttons wait for a preset and a keyset
Gating: the SCP80 "Send to Card", the RAM Execute and the TAR-probe Execute
buttons carry `data-needs-keyset`; pysimApplyAvailability() disables them
(with the hint "Select a card preset and its keyset" when the card itself is
fine) unless the view's preset is selected and the form's keyset number is one
of its keysets - `spKeysetStartReady`, the same rule the click-time guards
enforce, which stay as the last line of defence.  spKeysetSync() refreshes the
gate on every preset/keyset change, and the KIc/KID index selects now call
spKeysetSync() so the keyset selector follows a manual index change too.

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

Tests: indicator.test.js drives the gate (no preset -> disabled with the hint,
an undefined keyset -> disabled, a matching keyset -> enabled) and guards the
three buttons' attribute; the cards/keysets/ram sandboxes stub
pysimApplyAvailability.  Help EN/RU notes the disabled start buttons.
2026-10-01 22:03:36 +03:00
catarrh 39723406b4 ui: the preset list highlights the equipped card's preset
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.
2026-10-01 21:50:29 +03:00
catarrh 12da7ed593 ui: the keyset selector is disabled and cleared while no preset is selected
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.
2026-10-01 21:41:07 +03:00
catarrh cafec549fa feat: the Data Connection Status Change event gets its parameter form (0x1D)
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.
2026-10-01 21:35:23 +03:00
catarrh 0114c4748b ui: the STK menu button shows the title without the "STK: " prefix
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.
2026-10-01 21:23:06 +03:00
catarrh 9f7876b5fa docs: help/README catch up with v3.8.0; keyset-number wording; RU "пресет"
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.
2026-09-30 23:48:32 +03:00
catarrh 18e705a8c3 feat: TAR probe becomes its own SCP80 pill with a TAR checklist (v3.8.0)
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).
2026-09-30 22:36:03 +03:00
catarrh 6eeb8dacb3 feat: TAR probe + the SPI1 counter-tracking rule; keyless packets build again (v3.8.0)
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).
2026-09-30 21:17:59 +03:00
catarrh 386958b261 feat: warn when the equipped card and the selected preset disagree (v3.8.0)
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.
2026-09-30 15:37:51 +03:00
catarrh cc2edb3d8a feat: one keyset per key version - indexed keys and counters (v3.8.0)
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).
2026-09-30 15:15:21 +03:00
catarrh 795a1fea49 feat: card presets move to a server-side store; the server owns the SCP80 counter (v3.8.0)
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.
2026-09-30 13:59:47 +03:00
catarrh 162930eb96 feat: low-counter guard for every SCP80/RAM operation (v3.6.58)
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.
2026-09-30 11:10:54 +03:00
catarrh aa4bf8e008 feat: expanded registry on by default, form ladder + fallback report (v3.6.54)
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.
2026-09-30 09:44:39 +03:00
catarrh ee747003f2 feat: CAP compatibility test in the popup + per-AID failure analysis (v3.6.52)
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.
2026-09-30 09:22:22 +03:00
catarrh 599d84fd66 feat: Explore exposes the rest of the registry data + install NV footprint (v3.6.51)
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.
2026-09-30 09:00:33 +03:00
catarrh 0dc79a3ba4 feat: CAP compatibility test as its own operation + append-only imports (v3.6.48)
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.
2026-09-30 07:57:11 +03:00
catarrh cf9c9b7bab feat: clean up the Explore UX (v3.6.45)
- 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.
2026-09-29 23:34:42 +03:00
catarrh 3d4e4e81f8 feat: infer the ETSI release from the CAP's uicc.toolkit import (v3.6.44)
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.
2026-09-29 23:17:27 +03:00
catarrh f21d47538f feat: clearer import-probe wording, tooltips, translatable titles (v3.6.43)
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.
2026-09-29 23:07:25 +03:00
catarrh 755470e4ba feat: RAM-install import probe for LOAD rejections (v3.6.42)
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.
2026-09-29 03:41:07 +03:00
catarrh 904eca1758 fix: accept comprehension-required tag variants everywhere (v3.6.40)
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.
2026-09-29 01:54:39 +03:00
catarrh 0027c405c1 feat: answer GET INKEY / GET INPUT interactively (v3.6.38)
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.
2026-09-29 01:30:40 +03:00
catarrh ddf5cbb8e7 fix: late-PoR handler on every path, one poll per packet, LOAD-step components (v3.6.37)
_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.
2026-09-29 01:14:37 +03:00
catarrh f007d5c9be feat: CAP platform requirement + LOAD rejection hint (v3.6.36)
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.
2026-09-29 00:59:17 +03:00
catarrh a935361ca6 fix: clarify the suggested quotas, drop the CAP-details notes (v3.6.33)
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.
2026-09-28 23:57:59 +03:00
catarrh 85ba2a709d fix: explain the FF21 resource block and move the format line last (v3.6.27)
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.
2026-09-28 23:18:00 +03:00
catarrh 7c81f02308 feat: Explore expanded (E3) listing fallback + details pass + format display (v3.6.25)
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.
2026-09-28 22:51:20 +03:00
catarrh 5372a770eb fix: keep a hand-edited packet TAR in the SCP80 send (v3.6.21)
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.
2026-09-28 21:43:57 +03:00
catarrh b92a47cd44 fix: close the eSIM panel so the Test script pill shows its panel (v3.6.1)
#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.
2026-09-27 19:25:47 +03:00
catarrh 18a36a8f27 feat: generic BIP terminal control (Simulator BIP pill) (v3.6.0)
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.
2026-09-27 15:28:23 +03:00
catarrh b43e3118cf ui: give the Requires table its own AID column (v3.5.18)
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.
2026-09-27 13:11:28 +03:00
catarrh d37a29b389 ui: regroup the CAP report into Package / Requires / Memory tables (v3.5.17)
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.
2026-09-27 13:03:57 +03:00
catarrh 0255a956e2 feat: show the CAP's required libraries, package identity and components (v3.5.16)
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.
2026-09-27 02:23:03 +03:00
catarrh 18e0db75a9 feat: approximate NVRAM requirement from the CAP analysis (v3.5.15)
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.
2026-09-27 02:14:49 +03:00
catarrh d1c6a9c27a ui: rename the Phone simulator tab to Simulator and the eSIM pill to eSIM LPA (v3.5.12)
- 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.
2026-09-27 01:33:22 +03:00
catarrh 722e237ea4 feat: Test script pill in the Phone simulator (v3.5.11)
- 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.
2026-09-27 01:19:32 +03:00
catarrh 4c3fa64c95 feat: polling on by default and an effective-state toggle (v3.5.10)
- 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.
2026-09-26 13:09:44 +03:00
catarrh 25f001a778 feat: card-driven STATUS polling and the Poll Interval Negotiation event (v3.5.9)
- 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.
2026-09-26 12:56:18 +03:00
catarrh d9e6c6c9dd feat: CAP memory estimation with a confirm step before install (v3.5.8)
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.
2026-09-26 08:50:46 +03:00
catarrh b88f04fc69 feat: name standard package AIDs in the Explore / SCP81 / R-APDU views (v3.5.7)
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.
2026-09-26 00:33:38 +03:00
catarrh bed66c8ff8 fix: robust equip-state detection and review follow-ups (v3.5.4)
Code-review follow-ups for v3.5.2/v3.5.1, plus a test flake found while
re-running the suites:

- F1: a half-initialized equip is no longer reported as success.  cmd2
  swallows exceptions raised inside the equip command (and prints no
  traceback by default), while PysimApp.equip() assigns card/rs before it
  registers the command sets - so app.card alone once let the "CommandSet ...
  is already installed" abort pass as done while /api/tree stayed broken.
  The auto-equip attempt now captures the output with cmd2 debug on (a
  swallowed error prints a traceback), requires the new profile's command-set
  instances to be installed (_app_equip_complete), and the manual
  /api/command equip branch applies the post-equip refresh only when that
  check passes (and reports it in the output when it does not).
- F3: SCARD_E_SHARING_VIOLATION is recoverable (a rebuild cannot free another
  process's claim) instead of transport-fatal.
- F4: SPI1 b2b1 = 11 (Digital Signature) is refused instead of building an
  unsigned packet; help notes RC is CRC-32 only (KID CRC-16 not offered).
- F5: _clear_app_card_state removes the muted stdout again when the app object
  had no stdout attribute.
- F7: the watchdog re-arm keeps its rate-limit window when the trigger is
  busy/disabled instead of consuming it.
- F2: stale docstring in _auto_equip_attempt.
- tests: equip-state units, half-equip and captured-traceback failures,
  unequip-on-failure, busy trigger, sharing violation, DS refusal.
- bonus: the MCC/MNC random-pick test could fail because a dict keyed by
  (mcc, mnc) keeps one of two entries (the bundled list carries both a real
  and an MVNO entry for 234/18 and 234/28); the picker was correct - the
  assertion now checks the pair against the non-MVNO pairs.

549 frontend / 421 Python green; version 3.5.4; sw cache simple-v256.
2026-09-24 20:52:49 +03:00
catarrh 337e5df770 feat: decode the Items Next Action Indicator in proactive commands (v3.5.3)
SET UP MENU / SELECT ITEM items carry an optional Items Next Action Indicator
(TS 102 223 8.24, tag '18'): one byte per item, in list order, coded with the
Table 9.4 values marked "Used for Next Action Indicator".  The decoder ignored
it, so neither the proactive log nor the STK menu showed what the card would
do when an item is selected.

- server: NAI_TYPES whitelist (the ToC-only values - e.g. '26', '27', '47' -
  are reserved there and are ignored, as are '00' and unlisted values),
  _nai_name(), _attach_nai() (a short NAI list leaves the tail without an
  indicator, extra bytes are ignored); _parse_select_item() and
  _parse_setup_menu_items() plus the TERMINAL PROFILE SET UP MENU walk attach
  nai/nai_name to the items; _decode_cmd() renders '1. Menu -> SET UP MENU'.
- PWA: stkMenuNaiSuffix() adds a small gray '<name>' suffix to STK menu items
  that carry an NAI; the proactive log picks the decode up via cmd_decoded.
- tests: SET UP MENU / SELECT ITEM vectors with NAIs, a reserved value that is
  ignored, a short NAI list, and the stkMenuNaiSuffix unit test.
- docs: UICC_SPECS 6.5 gained the '18' Items next action indicator row
  (8.24/9.4); help EN/RU mention the menu suffix; AGENTS files updated.

548 frontend / 413 Python green; version 3.5.3; sw cache simple-v255.
2026-09-24 20:37:53 +03:00
catarrh 73ff6a60bd fix: SCP80 RC/CPL parity, file-life-cycle labels, EF.ARR and PS template decoders (v3.5.1)
Three fixes found while filling the ETSI/3GPP registry gaps against the
SIMalliance Stepping Stones R7:

- SCP80 builder/verification parity (TS 31.115 Table 1 NOTE / 4.2 / 4.3):
  the CPL is now transmitted whenever the packet is ciphered or carries
  RC/CC/DS - it is part of their input - and whenever the packet needs SMS
  concatenation; a single unprotected SM keeps pySim's CHL-first form.
  Before, the JS dropped the CPL for every unciphered packet while the
  server reference re-added it, so "Verify vs pySim" reported a false
  MISMATCH for every unciphered RC/CC packet (SPI1 01/02/0A/12/1A...).
  All ten offered SPI1 values now match the server reference byte-for-byte.
- RC (SPI1 b2b1 = 01) was offered but not built: the JS now computes CRC-32
  (TS 102 225 5.1.3.2, pySim zlib.crc32 parity) over the same CPL frame as
  the CC; the packet no longer silently omits the 4-byte RC field.
- Server: _build_secured_packet/_ota_reference add the CPL to a concatenated
  unprotected packet too (Table 1 NOTE / 4.3).

- fcpLifeCycle: unlisted values with b8 clear are RFU, b8 set is proprietary
  (Table 11.7b); previously all unmatched values were labelled proprietary.
- EF.ARR decoder now decodes the expanded format (AM_DO/SC_DO per ISO 7816-4
  5.4.3.2 + TS 102 221 9.2.7): operation bit masks, INCREASE/RESIZE AM_DO
  0x84, OR/AND/NOT templates, PIN key references with usage qualifiers.
- FCP 'C6' PS template DO decoded (PS_DO bitmap + key references + usage
  qualifiers, TS 102 221 11.1.1.4.10/9.5.2) with a shared key-reference map.

Tests: sp.test.js (CPL/RC vectors + crc32 known answer), ef_decode.test.js
(expanded-format ARR vectors), profiler.test.js (LCSI RFU/proprietary, C6),
test_ota_helpers.py (RC reference, unprotected single-SM vs concatenated).
Help EN/RU and READMEs: CPL size 2 (SMS), RC/CC/DS 4-8, RC bullet, CPL rule.
547 frontend / 397 Python green; version 3.5.1; sw cache simple-v253.
2026-09-24 08:05:07 +03:00
catarrh 7803d8520d docs: sync help/README with the Push commands, Parser and picker UI (v3.5.0)
- README/RUS: the Remote APDU tab has six sub-tabs, not seven; the GP
  connection-parameter table listed the wrong TLV tags (02/80/01) - it now
  matches the UI presets (82 Device Identities, 05 Alpha, 81 Command details,
  35 Bearer, 03 = default bearer).  The Push commands intro names the
  mode-specific rows (SD params tag in Store, Command Scripting template in
  Trigger) and the RU text uses the RU sub-pill labels; both READMEs gain the
  PoR advice bullet (Stepping Stones R7 18.6.3: no PoR on success, on failure
  SMS-SUBMIT + SPI2 0x20).
- help RU: the C-APDU parser section names the Parser pill sub-pills like the
  EN one; the Push commands intro describes what the mode actually switches
  and uses the RU sub-pill labels.
- sw cache -> simple-v252 (help pages are precached).

Docs only; no functional change, version stays 3.5.0.
2026-09-24 00:56:34 +03:00
catarrh 82f45d488f ui: guided OPEN CHANNEL parameters for the BIP opening request (v3.5.0)
The 01 (BIP channel opening) variant was raw hex while 03 (TCP) had a detailed
form.  The asymmetry is real - 01 accepts any OPEN CHANNEL COMPREHENSION-TLVs
(TS 102 226 9.2.1) while 03's set is fixed by 9.2.3 - so 01 now offers a
Fields/Hex switch instead of a rigid form:

- Fields (default) composes the most-used OPEN CHANNEL parameters per
  SIMalliance Stepping Stones R7 18.6.1: bearer description (default 35 01 03),
  transport protocol (UDP 01 / TCP 02, UICC client, remote - the only values
  TS 102 223 6.6.27.4 allows when the transport level is present) with the
  destination port, destination address, NAA/APN, buffer size, alpha
  (transparent checkbox -> 05 00 = no user confirmation) and an extra-TLVs
  field for login/password and anything else.  The local address is never
  emitted (9.2.1 and the guide forbid it).
- Hex keeps the raw COMPREHENSION-TLVs field; pushOpenChannelTlvs() is a pure,
  vector-tested composer feeding the same encoder as before.

Help 2.7 EN/RU and the READMEs explain the two modes; sw cache ->
simple-v251; version stays 3.5.0.
2026-09-23 22:58:22 +03:00