The browser Back button now walks the app's own view stack:
- a small state object {tab, sub, parser, view, listtab, preset, suite,
script, item} is recorded on every view switch (navRecord/navCurrent/
navStateKey) via history.pushState - session-only, the URL never changes
(pushState throwing on file:// degrades to no history);
- popstate replays through the same switch functions under a _navApplying
guard (they re-render and re-translate): the seven tabs, every pill set
(SCP80 / Remote APDU + parser / Simulator / SCP81), the Cards list/editor
(preset id), the test-suite views (suite/script ids) and the Profiler
views/list tabs (profile/snapshot ids); unresolvable ids fall back to the
area's default view;
- the in-app Cancel/Back buttons (cardsCancelEdit, testScriptBack,
testBackToSuites, profilerBackToList, profilerSaveProfile) call
navReplace() so their entry is replaced instead of stacked, and the
initial entry is seeded with replaceState so the first Back returns to
the initial view.
The reported untranslated suite Run button was the tip of a larger gap: the
sweep found 21 t() keys without a Russian entry (Run/Suite/Move/Copy/Warning/
Text, the respond-result names, the event-data errors, the CAP tooltip, ...)
- all added, and html.test.js now asserts every t('...') literal and every
data-l10n/data-l10n-title attribute has a LANG_RU entry.
Tests: nav.test.js (the recording/replace hooks, the state-key
canonicalisation, the router core), the i18n completeness test, the
extracted-function stubs in the cards/phone/profiler tests.
842 frontend + 819 Python; sw cache simple-v409.
The counter an operation consumed was re-entered by the PWA on every send
(the form field was filled from the preset and posted back), so a stale tab
could send a value the card rejects (cntr_low), and typing in the field
silently wrote the preset. Now the store is the single source of truth and
the form field is a per-send override (Option B).
- _preset_counter_seed: an explicit `cntr` in the request wins (a hand send,
the counter probe or a pre-built packet may deliberately pick one);
otherwise the preset store's value for the packet's keyset. Wired into
/api/send-ota (the APDU path; the pre-built `sp` path keeps the caller's
bookkeeping value - the packet carries its own counter), /api/ram-install,
/api/ram-install-app, /api/cap-compat, /api/tar-probe and /api/sp-verify
(the reference must use the counter the JS packet was built with).
- the send-ota response echoes the effective `cntr` (a caller that omitted
one learns what was used) and the OTA SEND log line prints it.
- persistence is unchanged and monotonic (presets.set_counter refuses
backwards writes), pinned by tests: an explicit lower counter is used for
the packet but never lowers the store, a higher accepted one is saved.
- PWA: the Secured packet counter field starts empty with the preset's next
value as its placeholder (auto: <cntr>); typing affects that send only and
is never written to the preset (spCntrSyncPreset is gone), spParams /
getRamSpParams omit an empty counter, the RAM/send flows sync only the
local preset copy (forward-only) instead of filling the field, the +/-
buttons start from the effective value, and the preview/verify packet is
built from the effective counter (genSp). Placeholder translated EN/RU.
- the runner and /api/counter-probe keep their existing seeding (the probe
already starts at the failed value and walks above it).
- docs: api.md's optional-`cntr` contract + seeding notes on every endpoint,
help EN/RU 3.1. Tests: 822 frontend + 734 Python; sw cache simple-v393.
The keyset table's column is headed "Counter (hex)" with the placeholder
0000000001, and cardsAdd refuses a save whose counter is not 1-10 hex
digits (empty included; whitespace stripped) - the same rule the store
enforces. The store keeps its import-compatible empty -> 1 default for
API/import callers.
Docs: repo AGENTS (v3.13.4 note). sw cache simple-v374.
Tests: 667 Python + 797 frontend green.
The release selector lives in the preset rather than a global chip: presets
already travel with the card (ICCID auto-selection, export/import), and the
operations that will consume the release are preset-aware.
- presets.py: the optional `release` field (4-18; empty = unspecified) joins
PRESET_FIELDS; a value outside 4-18 is refused on save/import; the store
schema version is 5.
- catrelease.py: DEFAULT_RELEASE = 12 and preset_release() - the effective
release is the preset's own value, else Rel-12 (the safe level for cards
whose release is not known; an unset release never filters anything away).
The PWA mirrors both (CAT_RELEASE_DEFAULT + presetRelease()), drift-guarded.
- Cards tab: the editor's header row gains a compact CAT release select
(unspecified / pre-Rel-6 stored as 5 / Rel-6 ... Rel-18); the empty option
names the Rel-12 default and the tooltip explains the guard; the field is
collected, cleared and restored with the other form fields.
- Nothing filters yet: the Simulator's event parameters (stage 3) will use
the effective release.
- docs/api.md + help EN/RU updated; sw cache simple-v368.
The keyset rows were a stack of flex divs with no column labels. They are
now a fixed-layout table like the TAR table: a thead with # (keyset number,
with its tooltip), KIc, KID, KIc key, KID key, Counter and the action
column, and <tr> rows of py-1 px-2 cells with w-full inputs (the columns
drive the widths: 40/56/56 px, the two key columns split the remainder,
112 px for the 10-digit counter). Ids and behaviour are unchanged (the
number span still toggles its red flag, the inputs keep their handlers).
Guards: html.test.js checks the keyset table headers and tbody;
cards_form.test.js checks the row markup. sw cache simple-v366.
Tests: 665 Python + 788 frontend green; the whole inline PWA script
syntax-checked.
The TAR table used the default auto layout with w-full, so the browser
distributed the surplus width across all columns and TAR/MSL/description
looked evenly spread. table-fixed with explicit column widths (TAR w-28,
MSL w-20, action w-12; the description column takes the remainder) and
w-full inputs keep the first two columns tight and left-packed. The ADM
placeholder is just "ADM" now. sw cache simple-v365.
Tests: 665 Python + 787 frontend green; the whole inline PWA script
syntax-checked.
- PIN/ADM: label left, input right (one line per code) - five rows instead
of the stacked label-above-input pairs.
- TAR rows: the TAR and MSL inputs are left-aligned like their headers and
the cells use the header padding (px-2), so values and headers line up
instead of the values floating centred.
- Add TAR and Add keyset: larger and green (bg-emerald-600) and both sit
below their lists, left-aligned (Add keyset moved down from the header
row; the "OTA keysets" label stays above the list). The TAR hint is a
plain paragraph under the button now.
- Guards: html.test.js pins the green/order of both buttons and the
label-left code rows; cards_form.test.js pins the TAR row alignment.
sw cache simple-v364.
Tests: 665 Python + 787 frontend green; the whole inline PWA script
syntax-checked.
- the ADM input capped the key at 8 chars, but the server's _verify_adm
takes 4-16 hex digits (padded to 8 CHV bytes) and the old input had no
limit; maxlength=16 now, with a static guard in html.test.js.
- cardsTarAdd/cardsTarRemove re-rendered the table from the save-path
reader, so an emptied role field was silently refilled with its default.
The renderer no longer invents defaults; a raw reader (cardsTarsFromTable)
feeds Add/Remove and the language-switch refresh, while the save path
(cardsTarsFromForm) keeps the default-filling for the mandatory roles.
- the editor's dynamic TAR/keyset rows now rebuild on a language switch
(refreshDynamicI18n) from what is typed - their placeholders and titles
carry t() strings. (The title/Save label were already refreshed there.)
- dropped the now-unused cardsRoleMsl helper and its test references.
- docs: the ADM is documented as 4-16 hex digits (help EN/RU, api.md,
AGENTS). sw cache simple-v362.
Tests: 665 Python + 784 frontend green; the whole inline PWA script
syntax-checked.
The Cards tab now shows the preset list by default; "Add preset" / "Edit"
open a separate editor view (Save stores and returns to the list, Cancel
discards; ramGoToPreset opens it too). The editor keeps the name/ICCID
header (with From card) and groups its fields into bordered blocks:
- PIN/ADM: the new optional card codes PIN1/PUK1/PIN2/PUK2 (plain text,
4-8 decimal digits) plus ADM, moved out of the top row; nothing consumes
the PIN/PUK codes yet.
- TAR: one uniform table (TAR, MSL, description); the first three rows are
the fixed role entries with the immutable English descriptions "ISD
compact format" / "UICC shared FS RFM compact format" / "ADF FS RFM
compact format"; further rows are deletable with an optional description.
- SCP80 (keyset editor) and SCP81 (PSK pair) as before.
Store: PRESET_FIELDS gains pin1/puk1/pin2/puk2 (optional; whitespace
stripped; 4-8 decimal digits validated on save and import), SCHEMA_VERSION
4, docs/help/AGENTS updated. TAR rows use unified ids (cards-tr-N-*), the
editor switches via cardsShowList/cardsShowEditor.
Tests: 665 Python + 783 frontend green; the whole inline PWA script
syntax-checked. sw cache simple-v361.
Presets drop the card-wide SPI1/SPI2 and the fixed tar/uiccTar/usimTar
fields: a preset now carries `tars` - the three mandatory role entries
(isd/uiccRfm/usimRfm, spec defaults pre-settled) plus free TARs with an
optional description, each with its MSL (Minimum SPI1, TS 102 226
8.2.1.3.2.4). v3.9.1 presets convert on load/import (the spi1 seeds all
three MSLs); duplicate TARs and empty TAR/MSL values are refused; the
store schema version is 3.
Operations derive a packet's SPI1 from its TAR's MSL (_spi1_for_tar): the
RAM ops, the test runner (per-step overrides still win), the counter probe
and each TAR-probe row; the plain send keeps an explicit SPI1 with a
`warning` when it is below the MSL, and a TAR outside the preset without an
explicit SPI1 is refused with a hint. The TAR probe resolves per checked
TAR before sending and reports the effective spi1 per row. The secured
packet form's SPI2 default is 00 (the RAM/probe operations pin their own).
PWA: the Cards form edits the TAR table (role rows with MSL inputs plus
Add/Remove free rows with a description), the list shows a TARs (MSL)
column, the SCP80 TAR field suggests the preset's entries (datalist) and a
preset apply seeds the SPI1 from the operation TAR's MSL with an amber
warning below it; the preset-wide SPI1/SPI2 fields and the header marker's
SPI requirement are gone (completeness = a keyset + the three role entries
with their MSLs). Help EN/RU, docs/api.md and AGENTS updated; sw cache
simple-v358.
Live: TAR probe 000000 at its MSL 16 (registered, por_ok, counter
advanced); a send without spi1 derived 16 (por_ok, counter advanced); a
send with spi1 0A returned the warning and the card refused it (6200, no
counter consumed); an unknown TAR without spi1 was refused with the hint.
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.
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.
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.
The SCP80/RAM forms hold a copy of the preset (counter, keys, TAR, SPI).
Editing the preset on the Cards tab saved correctly, but the form kept the
old copy: the operation sent the stale counter (the card answers cntr_low)
and the post-send sync wrote the stale value back over the preset - the
saved counter silently reverted. Reproduced in a real DOM:
after select in SCP80: preset=0000000001 sp=0000000001
after Cards edit+save: preset=00000000AA sp=0000000001
after a sync: preset=0000000001 (edit lost)
- `cardsApply()` split into `cardsApplyFields()` (field copy, no packet) and
`cardsApply()` = fields + genSp; new `spRefreshFromPreset(selId)` re-reads
the selected preset from `sp-card-sel` / `ram-card-sel` and re-applies it.
- `pysimSendOta()` and `ramExecute()` call it before starting, so every
SCP80/RAM operation uses the preset as it is now.
- A rejected send no longer advances the counter: new `spPorAccepted(por)`
gates the advance+write-back in `pysimSendOta`, the Explore pagination and
its GET DATA step, and the server's RAM install (`_ram_next_cntr`: advance
only for `por_ok`/`no_por` steps). A failed install still returns
`final_cntr` (the accepted prefix) and the PWA persists it, so a retry
never replays a counter the card already consumed.
- tests: cards_counter.test.js (the stale-form regression, RAM selector,
fields-without-genSp, spPorAccepted) and `_ram_next_cntr` cases; the
cards_form/ram harnesses updated for the split.
- docs/api.md counter semantics; AGENTS preset-source-of-truth rule.
607 frontend / 488 Python green; version 3.6.3; sw simple-v276.
- the ADM badge gains a trailing key glyph (ADM ✓ ⚿ / ADM ✗ ⚿) when the
preset matching the equipped card's ICCID carries an ADM key; the four
tooltip states cover key/no-key x verified/not-verified
- new gray SCP80/SCP81 markers after ADM, shown only when that preset has
all SCP80 fields (KIc/KID, SPI1/SPI2, counter, KIc/KID keys) or the SCP81
PSK pair filled in; hidden without a card session, a matching preset or
the fields, never color-coded
- cardsScp80Complete/cardsScp81Complete/cardsAdmPresent/cardsMatchedPreset
helpers; cardsRender reuses the SCP81 predicate
- tests: ADM four-state badge, marker show/hide, predicate whitespace
handling, header markup order; help EN/RU updated; SW cache simple-v201
- the SCP80 fieldset gains ISD TAR (000000), UICC TAR (B00000) and USIM TAR
(B00001), prefilled with the spec defaults; empty stored values mean the
default for that target (cardsTarValue)
- ADM field between "From card" and "Add", stored in the preset for later
use by the file manager (placeholder ADM (optional), spaces stripped)
- packing a chain selects the preset TAR for its target: RAM/GP -> ISD,
SIM RFM -> UICC, USIM RFM -> USIM (spec default without a preset);
_spTarKey keeps the target when the card preset is picked afterwards,
RAM card selection forces the ISD TAR
- USIM RFM 'from current DF' labelled (ADF.USIM); the list TAR column shows
all three values; help EN/RU updated
- SW cache simple-v199; new cards_form.test.js + html markup guards
(440 frontend / 281 Python tests green)