The card's contact pad sat mid-card; it now sits near the bottom edge,
away from the top-right cut corner (the cut side is the card's top).
No other geometry or animation changes.
1024 Python + 1063 frontend; simple-v473.
Redraw the inline card-into-reader SVG after sim.svg: a rounded card
with the top-right cut corner and the 3x2 contact pad, sliding behind
the reader bar. The viewBox grew to 72x48 and the resting position is
0, so the card is no longer clipped at the top; the insert slide is
20px. The currentColor/Tailwind theming and the reduced-motion
fallback are unchanged.
1024 Python + 1063 frontend; simple-v472.
A delete run whose DELETE answered 6A88/6A82 is still a success (the
object is gone, the goal is met) but it is no longer reported as a plain
"OK": the attempt result is 'already_absent', the counts gain that
bucket ("Deleted: N - Already absent: M"), the verdict reads "Already
absent" and the notice says the card was already absent. A reinstall's
tolerated pre-delete keeps the install outcome (the install is the
point).
1024 Python + 1063 frontend; simple-v471.
The waiting phase is the only moment the operator must act, so it now:
- shows a card-into-reader SVG animation in the status modal (inline
SVG, CSS keyframes in src/style.css; reduced-motion users get the
static card-above-the-slot picture);
- plays a short Web Audio chime once per card, edge-triggered in the
2 s poll so it never repeats (the AudioContext is unlocked by the
Start click);
- sets the tab title to "Insert next card" while the window is hidden;
- offers a speaker toggle next to the status (persisted as
simple_ram_batch_sound, on by default).
The prebuilt style.css was rebuilt: it had gone stale and lacked
utilities the UI already used (bg-blue-50 on the batch notice,
inline-flex, mx-2, max-h-48, w-44, min-w-24).
1022 Python + 1062 frontend green; simple-v470.
v3.34.0 was committed but never tagged; its changes ship in the v3.35.0
release, so the changelog names only released versions (the same rule
applied to 3.32.3 -> 3.32.4 and 3.32.6/3.32.7 -> 3.33.0).
The Read decoded pill used the same grey fill as a disabled control, so
an enabled pill looked unavailable (reported live). The inactive-but-
enabled state is now an outlined button (blue on white/slate) and the
grey fill plus dimming are reserved for the actually disabled state.
The active pill keeps a border of the same width so the row does not
shift.
Pinned in fs_edit.test.js. 1022 Python + 1058 frontend; simple-v469.
TS 31.102 4.2.27 scope: alpha; TP-DA and TS-SCA with their own length
conventions (DA = digits, SCA = octets, 0 = default SMSC) and a TON
select; TP-PID; TP-DCS with TS 23.038 hints; the relative TP-VP with the
TS 23.040 ladder; a per-parameter "absent (the ME default applies)"
state. Unchanged slots are copied verbatim (the live fixture's
DCS-absent 00 and present VP FF survive), alphanumeric TON-5 addresses
stay read-only, and every checked record is validated before any write.
Decoder fixes: the parameter-indicator bits were shifted (SCA on 5,
PID on 2, DCS on 3, VP on 4; the spec is DA/SCA/PID/DCS/VP on bits 1-5
with 6-8 reserved, 1 = absent), and the TP-DA length was read as octets
instead of digits (TS 23.040 9.1.2.5).
Live fixture: ffffffffffffffffffffffffffe9ffffffffffffffffffffffff07919733824009f0ffffffff0000ff
(SCA +79332804900, PID 00, DCS absent/00, VP FF) round-trips
byte-identically.
1022 Python + 1058 frontend green; simple-v468.
New client-side editors: the service tables EF.UST/IST/EST (a filterable
bit grid; unmodelled bits and the file length preserved), EF.ACC,
EF.PHASE, EF.HPPLMN, EF.AD (op-mode enum, additional info, MNC length
incl. 0 and the 3-byte SIM form), the PLMN-only lists
EF.PLMNsel/FPLMN/EHPLMN, EF.ICCID/EF.IMSI behind a confirmation, and the
cell-broadcast lists EF.CBMI/CBMID/CBMIR.
Framework: a bits form kind, a per-entry model(dec) hook, a per-entry
confirm prompt and hex fields.
Decoder fixes: EF.ACC class numbering (byte1 b8..b1 = 15..8, byte2 =
7..0), EF.AD operation mode (whole byte) and its MNC length rules,
EF.SST decoded with the SIM 2-bit allocated/activated layout (editing
stays disabled until that UI exists), and the EF.IMSI parity nibble
(9 odd / 1 even). A no-edit save of the new list files no longer
writes all-FF (the collected rows and the encoders disagreed on the
value shape).
Decoded editing for EF.DIR/EF.ARR/EF.SMS/EF.PBR is postponed by
decision (read-only viewing is enough); the survey records it.
1022 Python + 1033 frontend green; simple-v467.
Both versions were committed but never tagged; their changes ship in the
v3.33.0 release, so the changelog names only released versions (the same
rule applied to 3.32.3 -> 3.32.4).
The file manager gains a client-side decoded editor. EF_ENCODERS pairs
each editable EF with a pure encoder and a form schema (fields / list /
items); the file is read raw, the decoder fills the form, the browser
re-encodes it and the existing update path writes it.
Shapes: SPN (display-condition bits + Annex A text), PLMNwAcT/
OPLMNwAcT/HPLMNwAcT (rows with PLMN + ACT, add/remove),
ADN/FDN/SDN/MSISDN/LND/BDN (one form per record with a checkbox that
auto-checks on change; BCD, length and EXT handling; BDN's own X+15
layout) and PNN (per-record TLV items 43/45/80, add/remove, unknown
TLVs preserved).
Unchanged fields are copied verbatim (SSC/extended-BCD numbers, PLMN
rows including unmodelled satellite ACT bits, PNN codings/AddCI bits,
all-FF files, the BDN comparison-method byte), every checked record is
validated and encoded before anything is written, and edits that would
break a reference are refused.
Also: docs/decoded-editing.md (the spec survey behind the design and
the release waves), the efPlmnAct EC-GSM-IoT-only 0x88 fix, the PNN
spare-bit decode fix, and the /api/write|read cmd2-exception
false-success fix.
1022 Python + 1013 frontend green; simple-v466.
The decoded view is the client-side EF decoder's table over the raw
read's bytes. The collapsed "pySim JSON (server)" block was an
unrelated second decode - opening it ran another /api/read with mode
'decoded' (pySim's read_binary_decoded/read_records_decoded on the
card) - and it existed only when our decoder covered the file, so it
could not serve as a cross-check where one would matter.
/api/read keeps mode 'decoded' for API callers. Help, READMEs and the
project notes now state the table is built in the browser from the same
raw bytes; ef_decode.test.js pins the view's output and that no
mode 'decoded' read remains in the PWA.
1016 Python + 955 frontend green; simple-v465.
ramOpChanged() (v3.28.0) un-hid #ram-preset-warning by the operation
kind, but the banner's text and visibility belong to
spPresetWarningRender() (which already suppresses it for the batch
ops). With no warning text the empty amber box stayed visible until
the next status poll re-hid it - a flash on every RAM view / operation
switch. The visibility is content-driven again: the setHidden line is
gone and the renderer is called.
Pinned in frontend/tests/ram.test.js with the real renderer (the
pre-fix code fails the test). 1016 Python + 954 frontend green;
simple-v464.
The v3.32.3 changes (the traceback tail and the link-release wording)
were committed together with v3.32.4 and never tagged separately, so
the changelog named a version no release exists for. The entry is
merged into [3.32.4] and the published v3.32.4 release body matches it.
Live log: a card was pulled while the server started and the TERMINAL
PROFILE send raised CardConnectionException 0x80100016
(SCARD_E_NOT_TRANSACTED). The init's except caught it (no crash), but
the code then continued with the card object it had already built:
card_present and _CARD_CONNECTED became True - a phantom equipped card -
and the auto-equip's _CARD_CONNECTED early return then skipped every
later insertion, leaving only a manual Equip or a restart.
A PC/SC error in the startup init now prints one line (a traceback is
noise for a normal removal), clears the locals that feed those flags and
calls _clear_app_card_state(app); the presence monitor equips the next
card. Non-PC/SC errors keep their traceback.
Pinned in tests/test_startup_cardless.py (the branch and the flag
sources). 1016 Python + 953 frontend green; simple-v463.
v3.32.2 fixed the endless re-arm, but on Windows the first attempt
still failed on the old link (seen in the confirmation log: the
"self._con.connect()" traceback) and only the retry recovered - a failed
init, a 2-4 s delay and a traceback in the log.
The monitor's insert path now marks the transport stale under
_CARD_LOCK, so the first equip attempt rebuilds the PC/SC link. It also
covers a link created at a cardless startup (never connected). The
rebuild still happens in the equip worker, never in the pyscard monitor
thread, and _ensure_transport releases the old link first.
Docs: the READMEs' troubleshooting bullet.
Tests: an insertion marks the transport for the rebuild.
1014 Python + 953 frontend green; simple-v462.
After a card swap (or the first insertion after startup) on Windows the
auto-equip looped forever: "card inserted, initializing" / "card gone
during initialization", with no "retry 2/3" lines.
An equip with the card still present produced no card (app.card None),
and that branch reported "card gone" and returned True - a no-op success
- so the three retries never ran and the watchdog re-armed every ~5 s.
- The card-gone no-op is now only for a real removal (card_present
False); a card-present init that produced no card raises a failure.
- Any failed attempt marks the transport stale, so the next attempt
rebuilds the PC/SC link (a Windows swap can leave the old link unable
to connect; _ensure_transport releases the old link before building
the new one, so the 2026-09-24 stale-handle bug cannot recur).
- The error pySim swallowed (cmd2) is printed as "AUTO-EQUIP: init
output:" with its traceback - the failure is diagnosable from the log.
- Docs: the READMEs' troubleshooting bullet.
- Version strings + the PWA cache bumped together (simple-v460).
Tests: a card-present init without a card is a failure, a mid-init
removal stays a silent no-op, any failure marks the transport, the
stale-handle traceback reaches the log, and an end-to-end retry: a
failed init, the old link released, a fresh one built, the retry
succeeds. 1012 Python + 953 frontend green.
The import created presets with the store's default counter (1). Cards
whose counters are already past it had to be edited preset by preset
afterwards.
- The outfile preview form gains a "Start counter value" field (default
1, the store's value). The value is written into every created keyset
of every imported card - parse_outfile's `start_counter`, 1-10 hex
digits, whitespace stripped and uppercased; a bad value raises and the
endpoint answers 400. Without it the store's default still applies
(the field is omitted), so the API keeps its old behaviour.
- The preview shows the counter; an import stores the protocol's
10-digit form (counter_hex).
- Docs: api.md (the field), help EN/RU (the form).
- Version strings + the PWA cache bumped together (simple-v459).
Tests: the parser (every keyset, normalisation, unset, the bad values),
the endpoint (the preview's keysets, the 400, the stored padded form),
the PWA (the field's default, the controls' normalisation, both bodies).
1008 Python + 953 frontend green.
Reinstalling the package was slow because setup rebuilds pysim's
prerequisites (pyscard compiles from source on Linux), while a clone
updates with a plain `git pull`.
- tools/update.py (+ update.sh / update.bat in the package, and the
release workflow shipping them) updates a packaged install in place:
it downloads the latest release package, extracts it (zip-slip guard,
permission bits restored - zipfile drops them), replaces the app files
over the app directory and re-installs the app into the existing
.venv. The .venv and simple_lastrun.log are not in the package, so
pysim and its prerequisites survive: seconds, not a rebuild.
--check / --version / --zip (offline) / --no-pip / --force; the same
version is a no-op.
- CHANGELOG.md (Keep a Changelog, from the first packaged release;
earlier history is the git log). The release workflow now uses the
version's CHANGELOG section as the release body
(tools/changelog_section.py, falling back to generated notes).
- The READMEs document the update path and the `git clone --depth 1`
option for the clone path; setup prints the update hint.
- Version strings + the PWA cache bumped together (simple-v458).
Verified: 1007 Python + 953 frontend; the release-package test covers
the new files; the updater was exercised end to end (a v3.31.2 install
with a real venv updated to the 3.32.0 package: files replaced, the
venv marker kept, pip metadata 3.32.0, exec bits kept, a second run a
no-op, --check correct).
ssl's TLS-PSK callbacks (PskTlsServer) exist only since Python 3.13 - the
first CI run on 3.12 errored in all 20 PSK tests, and the docs confirm
"Added in version 3.13".
- PskTlsServer._make_context raises a clear RuntimeError naming the
requirement instead of AttributeError; a source pin keeps the guard.
- tests/test_scp81.py skips PskTlsServerTest where the callbacks are
missing (it runs on 3.13+), so contributors on 3.9-3.12 no longer see
20 failures.
- The READMEs state the requirement, and the Windows notes now strongly
recommend Python 3.13: pyscard has wheels up to 3.13, and on 3.14
pyscard and some pysim dependency wheels are unavailable and build
from source (MSVC). setup.bat and setup.sh print the same guidance.
- Version strings + the PWA cache bumped together (simple-v457).
998 Python + 953 frontend green.
The v3.31.1 run's frontend suite failed on sp.test.js: require('des.js')
(aes-js likewise) needs the npm dependencies, which exist in a working
tree but not on a fresh runner. The workflow now runs npm ci in
frontend/ (the lockfile is checked in) before node --test.
The v3.31.1 run's Python suite failed: ssl.SSLContext.set_psk_server_callback
(SCP81's PskTlsServer) was added in Python 3.13 - the job used 3.12 and all
20 SCP81 PSK tests errored. The workflow now pins 3.13; the package itself
is the tagged source and is unaffected by this CI-only change.
The first release run failed installing the test dependencies:
pyscard 2.3.1 publishes macOS and Windows wheels only (no manylinux),
so on Linux it always builds from source and needs the PC/SC headers -
the job stopped at "winscard.h: No such file or directory".
- The release workflow installs libpcsclite-dev before the dependencies.
- The READMEs and setup.sh name the header package next to pcscd/ccid:
libpcsclite-dev (Debian/Ubuntu), pcsc-lite-devel (Fedora), included
in Arch's pcsclite. This affects every Linux user running setup, not
just CI.
- Version strings + the PWA cache bumped together (simple-v456).
No runtime change: 997 Python + 953 frontend green; the release package
test builds with the new version.
The READMEs now point users at a release package instead of a clone; this
adds the machinery behind it.
- tools/build_release.py builds dist/simple-<version>.zip: the runtime
tree (the server package incl. its bundled MCC/MNC list, the PWA
runtime files, docs/, tools/, the setup/start scripts, pyproject,
requirements-pysim.txt, the READMEs, LICENSE and THIRD-PARTY-NOTICES)
under a single top-level directory, with no tests, no frontend
sources/build config, no bytecode and no git metadata. The version
comes from server.py; --expect-version guards a mislabeled tag.
- .github/workflows/release.yml runs both test suites, builds the
package and publishes it on a v* tag (workflow_dispatch for an
existing tag); a re-run uploads with --clobber. pysim is checked out
next to the repo for the Python suite and is NOT bundled.
- tests/test_release_package.py pins the package's shape: the runtime
files, the absence of the development files, and that sw.js's
precache list is fully covered - so a new asset cannot silently go
missing.
- dist/ is gitignored; the READMEs name the artifact.
Verified: 997 Python + 953 frontend green; the built zip (53 files,
~0.95 MB) was unpacked in a clean directory and its setup.sh installed it
into a fresh venv - the console script runs and the installed server
resolves web_dir to the unpacked frontend/ (index.html, sw.js,
uicc_files.json present). The workflow YAML parses; GitHub-side
execution is unverified until the first tag is pushed.
The READMEs told users to clone the repository even though most just
want to run the app. The Quick start now leads with the release
package (a snapshot of the tagged source: no git history, no
development files) and keeps the clone path for development or
unreleased changes; git is still required because setup installs pysim
from its upstream repository.
Windows notes refreshed against the actual tooling and PyPI:
- pyscard 2.3.1 ships cp39-cp313 wheels (no cp314) and requires
Python >= 3.9, so the pip/pyproject floor is 3.9 now (3.8 could
never install the dependency) and the "3.9 builds from source" note
is gone - only 3.14 does.
- The reader sentence now describes what actually happens: start.sh
asks for the first PC/SC reader when pcscd runs, the server
auto-detects a PC/SC reader itself, and with none present pysim
falls back to its serial default /dev/ttyUSB0 (use -d for a
serial/FTDI reader).
- The manual-install block is marked Linux/macOS: on Windows pysim is
installed via requirements-pysim.txt + --no-deps (setup.bat), since
the plain pip route pulls the SMPP bridge and its MSVC chain.
Docs/metadata only: 953 frontend + 989 Python green.
The server imports pySim (GPL-2.0-or-later - its source headers carry the
"or later" clause), and the PWA's EF decoders were ported/adapted from
simtrace-analyser (also GPL-2.0-or-later), so the whole repository is
distributed under the same license. Not bundling pysim changes only what
we redistribute (none of pysim's code - setup fetches it from upstream),
not the license of our own code.
- LICENSE: the GPL-2 text (the copy pysim distributes).
- pyproject: license = GPL-2.0-or-later + license-files, so the wheel
carries LICENSE and THIRD-PARTY-NOTICES in .dist-info/licenses/.
- THIRD-PARTY-NOTICES: pysim is a runtime dependency (not included); the
bundled MIT components keep their notices with the exact upstream
copyright lines (mcc-mnc-list, aes-js 3.1.2, des.js 1.1.0).
- READMEs: a License section (EN/RU) pointing at the above.
No functional change: 953 frontend + 989 Python green. The wheel
metadata was verified with pip wheel (License: GPL-2.0-or-later, both
license files present).
EF_PBR ('4F30', TS 31.102 4.4.2.1) lists the phonebook's files as
BER-TLVs, and those files are not in pySim's model - so they were
invisible in the file tree.
- The PWA gains the decoder (efDecPbr, registered as EF.PBR/4f30): the
'A8'/'A9'/'AA' type-1/2/3 containers, the 'C0'-'CC' kinds, the 02/03
value lengths (FID / FID+SFI) and the master EF (the first FID after
the first 'A8'), tolerant of FF padding, truncation, unknown tags and
the spec's vague 'AA' path form (kept raw + noted, never a throw).
- The server reads the three PBR copies once per card session
(best-effort, exactly like EF.ARR) into server.pbr and serves the raw
records from the card-free GET /api/pbr.
- The file tree is populated SERVER-SIDE (no frontend involvement): a
small Python parser reads the cached records and /api/tree appends the
referenced files as children of the DF that holds the PBR
({'name','fid','isDir':false,'pbr':true,'sfi'}, deduplicated against
the model), and /api/tree|select|read|write auto-allow the probe for
exactly those FIDs, so the injected nodes are clickable without any
PWA flag. The node-to-PBR match is by the requested node's normalised
path: a leading MF before an ADF segment is dropped and an ADF
addressed by AID (the Profiler's walk) folds onto its name, so both
DF_PHONEBOOK copies resolve to their own PBR; a full path never falls
back to another DF's copy, and an ambiguous bare 5F3A injects nothing.
- Drift guard: the JS and Python parsers share
frontend/tests/pbr_vectors.json, which includes the TS 31.102 Annex G
record (its printed A8 length is internally inconsistent - 10
primitives x 5 bytes = 0x32, not the printed 0x2D - so the corrected
length is used, noted in the case name).
Docs: api.md (/api/pbr, the tree children, the auto-probe, the AA path
form), the repo AGENTS. 953 frontend + 989 Python; sw simple-v455.
The independent re-check of the v3.30.4 identity fixes found a
pre-existing gap in testSuiteDelete: it cleared the selection only when
the deleted suite WAS the selected one, so deleting a suite BEFORE the
selection left _testSuiteIdx pointing at whatever moved into that slot
(or out of range when the list shrank).
The delete now captures the selected suite's id just before the store
refresh and re-resolves the index after it, which also keeps a selection
the user made while the dialog was open (the modal no longer freezes the
page).
Test: a behavioural test drives the real function through the four cases
(the selected suite deleted; a suite before it deleted; the selection
moved during the dialog; nothing selected) - mutation-checked to fail
without the re-resolution. 946 frontend + 952 Python.
The native browser dialogs block the page and look foreign inside the
app. One shared modal (#pysim-dialog-modal) with the promise-based
helpers pysimAlert/pysimConfirm/pysimPrompt replaces all 71 call sites
(60 alert, 10 confirm, 1 prompt in profilerNew); deferredPrompt.prompt()
is the PWA install prompt and is untouched.
The helpers serialize through ONE promise queue (overlapping dialogs
appear in order, no message is overwritten). Escape cancels; Enter
accepts except with the Cancel button focused (native behaviour); key
repeats/IME are ignored; a POINTER answer within 500 ms of a dialog
opening is dropped (the buttons keep the same position, so a
double-click's second click would otherwise answer the next queued
dialog - e.g. an alert queued before a destructive confirm). Backdrop
clicks are ignored. The dialog is role="dialog" aria-modal="true" and
traps Tab (the handler cycles the controls the dialog shows: alert OK;
confirm Cancel/OK; prompt Cancel/input/OK), so the focus can neither
leave the modal nor reach a background control (Space would activate
it). When the modal is absent the helpers fall back to the native
dialogs - the Node tests extract single functions without the markup.
Alerts are fire-and-forget (a notification, not a decision); the 10
confirms and the prompt are awaited, which made five functions async
(scriptsDelete, pysimCustomRemove, profilerNew, profilerDelete,
snapshotDelete). Because the modal no longer freezes the page, the
delete flows that mutated by index after the confirmation mutate by
identity now (profilerDelete/snapshotDelete/pysimCustomRemove/
testSuiteDeleteScript/testSuiteDelete): a list reorder during the open
dialog would otherwise delete the wrong entry.
The nine dialog messages that were still raw English literals are
t()-wrapped - three of them (No secured packet to verify/send, Card
reader server is not connected) activated RU entries that had existed
unused because they were never wrapped; Import error and Snapshot save
failed are new keys.
Drive-by: the batch modal's #ram-op-batch-info used whitespace-pre-line,
which has no rule in the prebuilt style.css (the v3.30.0 stale-style
trap), so its notice never line-broke - now whitespace-pre-wrap.
Tests: a new frontend/tests/dialogs.test.js (15 tests: the modal path,
the queue, the double-answer guard, the keyboard, the grace, the trap,
the fallback, the sweep pin, the stylesheet pin) plus the async/helper
updates in 8 existing test files. 945 frontend + 952 Python;
sw cache simple-v454.
Under start.bat the server's stdout/stderr are pipes into PowerShell's
Tee-Object, so Python encodes with the ANSI code page instead of the
wide console API. cp1251 (Russian Windows) cannot encode the banner's
box-drawing rule (U+2500), and that print killed the server with
UnicodeEncodeError before it could listen.
- _guard_console_streams() (the first statement of main()) sets
errors="replace" on both streams: an unencodable character prints as
'?' and can never raise.
- _console_rule() picks the widest banner rule its stream can encode -
U+2500 where the code page has it (utf-8, cp437), ASCII dashes
otherwise (cp1251, ascii).
- The seven text-layer trace strings are ASCII now: the APDU arrows
(U+2192) and the INIT/CAP-COMPAT em dashes (U+2014 - cp437 cannot
encode them either), so the traces stay readable on any code page.
The raw os.write paths (_log_stdout, the APDU tracer) bypass the text
layer, cannot crash, and were left as-is.
Tests: tests/test_console_encoding.py - the strict-cp1251 control, the
rule's fallback/pretty cases, the guarded banner+traces, a stream
without reconfigure, the main() source pin, and an AST pin that no
print/stdout/stderr literal in the package is non-ASCII.
tests/test_startup_cardless.py's CARDLESS_LINE follows the new ASCII
line.
Verified with PYTHONUNBUFFERED=1 PYTHONIOENCODING=cp1251: the pre-fix
code died at the banner, the fixed code reaches the listening banner
and serves a command (CMD: -> OK, no encode errors).
930 frontend + 952 Python; sw cache simple-v453.
The v3.30.1 review follow-up. The sweep fixed the two batch-state
strings but two more shared (kind-independent) batch strings were still
install-specific, so a delete/reinstall batch started under an "install"
label:
- 'Start batch' was «Запустить пакетную установку» - the ram-batch-start
button starts every kind - now «Запустить пакетную операцию» (aligning
with the optgroup «Пакетные операции (по картам)»).
- the shared panel hint's «...перезапустите пакетную установку» is now
«...перезапустите пакетную операцию» (the EN was already neutral).
The RU help quoted the old button label: its install row now says
«Запустить пакетную операцию не требует карты», and the row's other
generic "the batch" statements (never retries, the test-run exclusion)
are neutral too; the genuinely install-specific ones (the workflow, the
shared install form) stay.
The v3.30.1 notes were corrected: the counts labels do follow the last
run's kind after a clear (the values stay zero) - the AGENTS entry and
the _ramBatchLastKind/ramBatchRender comments say so now. The
cleared-title test seeds the state before opening the modal (no reliance
on the previous test's sandbox leftovers).
930 frontend + 945 Python; sw cache simple-v452.
The v3.30.0 review follow-up, both items cosmetic:
- The modal's title follows the run's kind, but a cleared report carries
kind: null, so Clear flipped a delete/reinstall run to the install
title. The last seen kind is remembered (_ramBatchLastKind, set in
ramBatchRender) and the title falls back to it through the new
ramBatchModalTitle() helper.
- The two batch-state RU strings stopped being install-specific: 'Batch
stopped'/'Batch finished' are now «Пакетная операция
остановлена/завершена» (the v3.29.0 kinds can delete or reinstall),
matching the help's own «Пакетная операция» wording.
Test: the cleared title keeps the kind and a fresh page keeps the
install default. 930 frontend + 945 Python; sw cache simple-v451.
A running batch now shows in a modal window (the same chrome as the
single-operation modal): the live status on top - the current step from
ram_progress (Step n/N - <step>, with the bar) or the waiting prompt
("Insert next card. Waiting for a card...") - plus the counts and the
last finished card's verdict, and the processed cards below (the same
attempts table with the per-step verdicts).
It opens on Start and on a page load with a run in flight (the first
poll sees ram_batch.active); Close is disabled until the run ends (the
run is server-side and survives a reload - that is what reopens the
window), and Stop/Clear live inside it (Clear only once the run ended).
The panel's new Show status button reopens a finished report. A single
operation takes the modal back; ramBatchRender feeds both the panel and
the modal (the poll's fetch decision now runs for either).
Docs: help EN/RU, the repo AGENTS. Tests: the open on Start and on the
poll, the live status/bar/counts/attempts, the close guard (active vs
ended), the reopen button. 929 frontend + 945 Python; sw cache
simple-v450.
The format is positional: an empty value drops its token, so the parser
now requires every data row to have exactly the header's column count.
That catches an empty value anywhere - including a middle one, which
would otherwise shift the following columns (a missing ADM made the KIC
columns read the KIK values: valid hex, wrong preset) - and a KICn/KIDn
pair declared in the header must carry a usable hex value in every row
(a placeholder fails).
A violation makes the whole file invalid: the report's error names the
line, no presets are returned, and an import (mode) answers 400 - it is
never partially imported. The PWA disables the Import button while the
report carries an error and guards the call.
Unchanged per-row reports: a non-hex PIN/PUK/ADM/PSK value (dropped, the
card kept), an unsupported key size (that keyset skipped), a card whose
remaining keysets are unusable (skipped). All three real samples satisfy
the rule exactly (11/11, 18/18, 18/18).
Docs: help EN/RU, api.md, the repo AGENTS. Tests: the short/long row and
the placeholder-keyset file errors, the valid-file round trip, the
endpoint's refusal (nothing stored), the PWA's disabled button.
926 frontend + 945 Python; sw cache simple-v449.
The import applied one global default keyset to every TAR entry of every
imported card. The preview's TAR table (TAR + MSL, the three role rows +
"+ TAR") now has a keyset select per row, mirroring the preset editor: an
empty "— none —" plus the keysets found in the file with their KIc/KID,
the first preselected, and the editor's amber warning when a non-zero MSL
has no keyset. Each TAR's packets then use exactly the chosen keyset.
- PWA: the global "Default keyset for the TARs" select (and the request's
tar_keyset) is gone; cardsOutfileTarsFromDom/FromForm carry each row's
keyset; cardsOutfileKeysets() became cardsOutfileKeysetOptions() (the
union with the KIc/KID bytes for the labels).
- Server: a `tars` entry's own `keyset` (1-15; an unusable value is a 400)
wins, then the optional global `tar_keyset` (the API shorthand), then the
card's first keyset - parse_outfile no longer clobbers per-entry keysets
with the card-level fallback. presets.normalise_keyset_ref() is
module-level (one rule for the store and the parser).
- Docs: help EN/RU, api.md (the tars entry keyset + the global fallback),
the repo AGENTS. Tests: the per-entry/global/card fallbacks, the
bad-keyset 400, the options helper, the row select and round-trip, the
import body. 925 frontend + 944 Python; sw cache simple-v448.
The batch plan is a list of named steps ({name, apdu, delete}) now and
_ram_install_run generalized into _ram_plan_run; POST /api/ram-batch/start
dispatches on kind:
- install-cap (default; delete_package prepends a cascade DELETE on the
package AID - a leftover package blocks INSTALL [for load]),
- reinstall-app (_ram_reinstall_plan: DELETE the applet instance + INSTALL
[for install] from the loaded package - no .cap, no LOAD),
- delete (_ram_delete_plan: one GP DELETE, aid + target app/package ->
P2=00/P2=80).
A delete step tolerates 'not found' (6A88/6A82 - the object is already
gone, the goal is met); any other delete failure stops the chain with the
delete's reason. The policy serves all three kinds and the single
/api/ram-install-app, which now deletes the instance first (make
selectable stays one step); /api/ram-install takes delete_package too.
The status/summary carry the run's kind (the panel labels follow:
installed/deleted/reinstalled) and each attempt carries the last three
steps (steps + steps_total) for the panel's per-step detail. The PWA's
three batch entries are grouped at the end of the operation selector (an
optgroup); ramIsBatchOp() is the one source for "per-card batch".
Docs: help EN/RU, api.md, the repo AGENTS. Tests: the plan builders per
kind, the validations, the delete-package prepend, the not-found
tolerance, the kind dispatch/status/notice, the steps tail, the endpoint
steps, the PWA wiring. 925 frontend + 943 Python; sw cache simple-v447.
The attempts box's content is replaced on every render (the batch panel is
static markup; the render fills the dynamic bits) and replacing a
scrollable element's content resets its scroll. That was harmless while
the render ran once per attempt, but v3.28.7 made it run on every 2 s
poll, so scrolling back through the attempts during a run snapped to the
top each poll.
The render now captures the box's scrollTop before refilling it and
restores it afterwards.
Test: the stub box resets its scroll on an innerHTML write (like a
browser) and the render must keep the reader's position - verified to
fail without the restore. 920 frontend + 928 Python; sw cache
simple-v446.
ramBatchFetch's re-entrancy guard (_ramBatchFetching) is only cleared
when the fetch settles, and ramBatchApplyStatus returned without
rendering whenever it decided a fetch was due - so one stalled
/api/ram-batch/status request (the browser's fetch has no timeout) froze
the whole panel (counts, phase, notice, throbber) at the last successful
render while the server kept installing (live 2026-10-14: 31 installs,
the panel stuck at "Installed: 12"; the log showed all 31 attempts and a
healthy /api/status cadence, so the data was there).
- The summary render now runs on every poll before the fetch decision:
the headline (phase, current card, counts, notice) always follows the
2 s /api/status poll; the detailed fetch only adds the attempts table.
- A fetch guard older than RAM_BATCH_FETCH_STALE_MS (10 s) is released,
so a stalled fetch cannot block the report forever - the next poll
retries and the attempts table recovers.
Reproduced with the page's real functions: 12 good fetches, the 13th hung
-> the panel stuck at 12 (the reported number); with the fix it follows
the summary to 31 and the stale guard retries. Tests: a hung fetch keeps
the counts following the summary; a stale guard is released and retried
(a fresh one is kept). 919 frontend + 928 Python; sw cache simple-v445.
A card may deliver the response packet (the PoR) in the GET RESPONSE of
an ENVELOPE that answered 61xx and signal a proactive command pending in
the same answer (91xx - the CAT success state, not an error).
_send_envelope ran the chain and then cleared the response data
(`data, sw = '', '9000'`), so the operation reported no PoR although the
card had returned one (live 2026-10-14: a GP DELETE answered 6113, the
GET RESPONSE delivered the PoR with 9130, and the delete verdict showed
"ok (no PoR)").
The delivered bytes are kept now; the chain still runs and the exchange
reports 9000. Test: the delivered PoR survives the chain (verified to
fail on the old code). 917 frontend + 928 Python; sw cache simple-v444.
The panel's hint read "«Остановить» завершает текущую карту" - a poor
translation of "Stop finishes the current card first". It is now
"«Остановить» завершает работу после окончания обработки карты", and the
help row's phrase is aligned ("...после окончания обработки текущей
карты"). The EN text is unchanged.
917 frontend + 927 Python; sw cache simple-v443.
The profile rule editor rendered the same "FCI hex (raw SELECT response)"
field for Exact and Custom, although the custom check never reads
`fciHex` (it compares the data objects against the card's live FCI) - so
the field looked like an expectation it was not.
- The raw-FCI field and its decoded preview are exact-mode-only now; the
Custom mode shows just its data-object editor.
- Switching a rule to Custom seeds the data objects from its captured FCI
when the list is empty (the same all-'value' seeding a captured profile
gets); the "load from the captured FCI" link refills an emptied list,
and a rule without a captured FCI still starts empty for manual rows.
From the v3.28.3 review: the ADM's byte-pair rule now lives in one
place, presets.ADM_PATTERN, used by /api/verify-adm and the outfile
import - two literals could drift and an imported ADM could silently
become unverifiable. The RU help's ADM wording is "4-16 hex-цифр
(чётное число)".
Docs: help EN/RU, the repo AGENTS. Tests: the mode switch (seed / keep /
no FCI / switch-away), the field-gating source pin, the pattern's shape
and the both-users pin. 917 frontend + 927 Python; sw cache simple-v442.
The review of v3.28.2 found the parser accepting an odd-length ADM (e.g.
12345), while /api/verify-adm requires byte pairs
((?:[0-9A-F]{2}){2,8}) - the stored value could never verify and was
refused with a misleading "adm must be 4-16 hex digits".
The parser now applies the verifier's exact rule: an odd length (a half
byte) or a value over 16 digits is dropped at import time with "ADM is
not 4-16 hex digits in byte pairs".
Docs: help EN/RU, api.md and the repo AGENTS say "4-16 hex digits in
byte pairs". Tests: the odd-length and the over-16 drop cases (both
real samples are even-length and unaffected: BAP 3632333239463337,
AM0027 CFA3DD9C6B7F0F8C). 915 frontend + 925 Python; sw cache
simple-v441.
The ADM column carries the key's hex value (the length varies, 4-16 hex
digits) - the form the preset store and /api/verify-adm use (pySim pads
it with 'f' to the 8 CHV bytes). The parser treated it as an ASCII
string hex-encoded and either mangled it (3632333239463337 -> 62329F37,
a wrong ADM that would fail VERIFY and consume an attempt) or dropped it
(CFA3DD9C6B7F0F8C - the live report with AM0027_alfa_dsa.out).
- The ADM is stored as-is: non-hex stripped, uppercased, [0-9A-F]{4,16};
a non-hex or too-short value is dropped with "ADM is not 4-16 hex
digits".
- _ascii_from_hex() is gone (its only user was the ADM column).
- Docs: help EN/RU, api.md and the repo AGENTS now say "the key's hex
value, 4-16 hex digits (the length varies)".
Note: presets imported from a BAP-style file before this fix hold the
mangled ADM - re-import or correct them.
Tests: the BAP fixture (the hex as-is), the AM0027 shape, a
short/lowercase value and the drop cases. Both real samples verified
(BAP 3632333239463337, AM0027 CFA3DD9C6B7F0F8C; the ALFA sample has no
ADM column and is unchanged). 915 frontend + 925 Python; sw cache
simple-v440.
- A throbber (#ram-batch-spin) next to the phase line shows while a card
is actually being installed (active && current - the stopping-mid-
install phase included) and hides again while waiting.
- The waiting prompt is explicit now: "Insert next card. Waiting for a
card..." (RU "Вставьте следующую карту. Ожидание карты…"); the old
"Waiting for a card..." key is gone.
- The notice reports only the last *finished* card: starting the next
card's install clears it (state['notice'] = '' alongside
current/installing in _ram_batch_handle_card), so the previous card's
verdict never lingers over the next install. It stays visible while
waiting - the "report the status + ask for the next card" behavior.
- The batch operation's RU label is "Пакетная установка (.cap на
множество карт)"; the help row's title aligned.
Tests: the render test (the new prompt, the throbber visible while
installing / stopping-mid-install and hidden while waiting, an empty
notice hiding the box) and the worker test (the notice is empty
mid-install and set again for the new outcome). 915 frontend + 925
Python; sw cache simple-v439.
The review of the connection-check fix found the second-order race it
opened: _apply_equipped_card sets _CARD_CONNECTED = True and bumps
card_session *before* it clears and reads the ICCID (and long before the
network state / TERMINAL PROFILE / drain finish). A worker poll landing
in that window saw connected + a fresh session + iccid = None and
consumed the card with a bogus 'no ICCID' skip - the card was never
installed. The `equipping` flag could not guard it either: the manual
/api/command equip path never sets it.
The fix marks the finished init instead of guessing:
- _apply_equipped_card sets `server.card_ready_session =
server.card_session` as its last statement;
- the startup init (which inlines the post-equip work and never runs
_apply_equipped_card) sets the same marker;
- the worker requires the marker to match the card session, so it acts
only on a fully initialized card - a genuinely unreadable ICCID still
acts and reports its skip, as designed.
Tests: a worker test that keeps the marker stale for a couple of polls
(no attempt - verified to fail without the guard), the marker pins for
both equip paths, and the fake server carries the marker (a real
attribute) instead of inventing state. The unused `server` parameter of
_ram_batch_start is gone, and the two comments that called this work a
"review" now say audit.
915 frontend + 924 Python; sw cache simple-v438 (unchanged).
The v3.26.0-v3.28.0 audit found the worker reading
getattr(server, 'connected', False) - an attribute the server object has
never had. The connection state is the module global _CARD_CONNECTED
(set by __main__ / _apply_equipped_card / _handle_card_disconnect and
read by /api/status the same way), so the check was always False and a
batch never acted on an equipped card: it would wait forever. The unit
test hid it - the fake server invented a `connected` attribute the real
object does not have.
- The worker reads _CARD_CONNECTED. The fake server carries only
attributes the real object has, the worker test patches
_CARD_CONNECTED (verified to fail on the old line), and a source pin
keeps the worker off server.connected.
- The write-only `thread` field of the batch state is gone (nothing read
it).
- The outfile endpoint refuses a non-list `tars` with 400 instead of
silently falling back to the role defaults.
915 frontend + 922 Python; sw cache simple-v438 (unchanged).
- A non-ValueError exception from the install run (a card removed
mid-chain, a transport error) was recorded as a 'skipped' attempt with
the error text - but the chain had started and died, which is a
failure. Only a ValueError (a request/eligibility error: the preset's
ISD keyset, the plan) is a skip now; anything else is a 'failed' record
carrying its error. The PWA's failed row falls back to that error when
there is no failing step / status word to show.
- The panel's Clear button is disabled while a batch is active (the
server refuses the clear with 409 anyway) - no pointless error alert.
- api.md and the repo AGENTS document the two outcomes. Tests: a
transport error mid-chain is a failure (and the card stays seen), the
plan's input validation (the block size, a missing cap_hex), the failed
row without a step, the shared ram_format field, and the Clear state.
915 frontend + 921 Python; sw cache simple-v438 (unchanged).
The bulk workflow: one .cap and one set of install parameters applied to
card after card - swap cards and each one installs as soon as it is
equipped. The server owns the loop, so closing the tab or reloading the
page does not interrupt it.
The install chain is now shared: _ram_install_plan(body) builds the
card-independent part (the parsed .cap, the INSTALL/LOAD sequence, the
block size, the transport knobs) and _ram_install_run(server, body, plan,
scc) runs one chain (the per-card preset/keyset/counter resolution
included); the /api/ram-install handler is the plan + run + response, and
the batch worker calls the same pair. The plan is built once at Start,
so a bad .cap or parameter combination is a 400 there (a malformed
archive included - a zip parse error is the caller's, never a dropped
connection), not a per-card failure. _op_keyset_resolve() is the module
form of the handler's _op_keyset() (the dead _keyset_guard method is
gone).
The worker (_ram_batch_worker, state _RAM_BATCH) polls the card state
every 0.5 s and acts on a new card session while connected; a card
already equipped at Start installs immediately. Per card the preset is
matched by ICCID and its ISD role TAR supplies the keyset (v3.26.0) and
the counter seed (v3.20.0); a card with no readable ICCID, no matching
preset or no usable ISD keyset is recorded as skipped with its reason.
A card is attempted once per run (any outcome): re-equipping it records
"already processed in this batch (<outcome>)" and never installs again -
restarting the batch is the retry. Failures carry the failing step and
the remote SW, are never retried and never stop the run; an unexpected
card-handling error is recorded and the loop keeps waiting.
Endpoints: POST /api/ram-batch/start (no card needed; 400/409), GET
/api/ram-batch/status (card-free - the panel polls it during an install),
POST /api/ram-batch/stop (the first _CARD_FREE_POST: it only sets a flag,
so the button works while the worker holds the card lock; a running
install always finishes first) and POST /api/ram-batch/clear (409 while
active). /api/status carries the ram_batch summary (the detailed state
minus the attempts list), so the panel's headline - the live phase, the
current card, the counts - rides the 2 s poll; the attempts report (the
last 500 kept, the total counted) is fetched only when it changed. The
batch and a test run exclude each other (409 both ways); the state is in
memory - a server restart ends a run.
PWA: the Batch install (.cap file, per card) RAM operation shares the
install form (ramInstallCapFields(); the single install uses it too and
now sends ram_format, and the block-size validation moved before the
modal), hides the preset selector + Execute and the preset-mismatch
banner, and renders Start/Stop/Clear, the phase headline (equip the next
card / installing card + live step / stopping after the current card),
the counts and the attempts table (newest first). The panel only works
inside the visible SCP80 RAM pill.
Docs: help EN/RU, api.md (a section + the four table rows), the repo
AGENTS. Tests: the state/report helpers, the per-card decisions, the
worker loop (a real thread + a fake install), the lifecycle and guards,
the HTTP endpoints (the malformed .cap, the stop under a held card lock,
the test-run refusal) and the PWA panel helpers/wiring. 914 frontend +
919 Python; sw cache simple-v438.
- The endpoint validates algorithm16 (3des2/aes, case-insensitive) and mode
(merge/replace or omitted): a misspelled value answers 400 instead of
silently picking 3DES2 or turning an import into a dry run. The choices
come from outfile.ALGORITHM16_CHOICES, derived from the algorithm map, so
the validation and the parser cannot drift apart.
- A present but unusable PIN/PUK value is reported with its raw text: a value
that strips to nothing (e.g. 'abcd') used to vanish without a report line,
contradicting the "nothing is silently dropped" contract. A column absent
from a short row stays silent - it is not present.
- The column-line detection drops the arbitrary 8-character column-name cap:
a candidate header must carry ICCID plus at least one column the import
consumes (a fixed name or a numbered KIC/KID), so a long unknown column
name next to a real one parses while 'ICCID <value>' and
'ICCID CUSTOMER_NAME' config lines are still rejected. Both real samples
are unaffected.
- PWA: a chosen default keyset survives an empty or failed preview (it used
to reset to null, and the import silently fell back to the card's first
keyset); the select renders the preview's keysets plus a remembered value.
- PWA: the destructive Replace mode asks for confirmation first, matching the
other store-wiping actions.
- api.md documents both validations. Tests: the endpoint's 400s and the
replace import, the raw-value report, the header guards, the keyset
preservation and the declined confirmation. 906 frontend + 904 Python;
sw cache simple-v437.