v3.28.0: the batch CAP install

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.
This commit is contained in:
2026-10-14 15:00:03 +03:00
parent 21d7f58f61
commit ab373aff30
13 changed files with 1527 additions and 236 deletions
+66
View File
@@ -46,6 +46,10 @@ a 3.x PWA).
| `/api/help` | POST | pySim help for a given command |
| `/api/send-ota` | POST | SCP80 OTA secured packet delivery |
| `/api/ram-install` | POST | Install a Java Card `.cap` file via SCP80 (INSTALL[for load] → LOAD ×N → INSTALL[for install]) |
| `/api/ram-batch/start` | POST | Start the **batch CAP install** (v3.28.0): the server installs the same `.cap` on every card that gets equipped, using each card's preset (matched by ICCID) |
| `/api/ram-batch/status` | GET | The batch state: phase, current card, counts and the attempts report (card-free) |
| `/api/ram-batch/stop` | POST | Stop the batch — a running install finishes first (card-free, so it works while the install holds the card lock) |
| `/api/ram-batch/clear` | POST | Drop the finished batch's report (`409` while a batch is active) |
| `/api/ram-install-app` | POST | Single INSTALL [for install] / [for make selectable] for an already loaded package (no re-load) |
| `/api/cap-info` | POST | Validate a `.cap` archive and estimate its code/NVRAM/RAM requirements (read-only) |
| `/api/cap-compat` | POST | CAP compatibility test: LOAD only to the block completing the Import component (nothing committed) |
@@ -1126,6 +1130,68 @@ sets `success: false`, `failed_step` and a detailed `error` such as
The `steps` array contains one entry per GP command. `final_cntr` is the counter value after all successful steps (use it to update the card preset). The response is not streamed — all steps run server-side before the JSON is returned.
### `POST /api/ram-batch/start`, `GET /api/ram-batch/status`, `POST /api/ram-batch/stop`, `POST /api/ram-batch/clear`
The **batch CAP install** (v3.28.0): one `.cap` and one set of install
parameters applied to card after card, for bulk provisioning. The loop runs
**in the server** (a worker thread), so a page reload or a closed tab does not
interrupt it; the state lives in memory and a server restart ends the run.
- `POST /api/ram-batch/start` — the body is the `/api/ram-install` payload
**without** the per-card fields (`preset_id`, `tar`, `spi1`, `cntr`,
`kic`/`kid`/`kicKey`/`kidKey`): the plan is card independent and is built
once, so a bad `.cap` or an invalid parameter combination is a `400` at
Start, not a per-card failure. **No card is needed** — the worker waits for
one. `400` for a bad request, `409` when another batch or a test run is
active. The response is the status payload below plus `ok: true`.
- `GET /api/ram-batch/status` — **card-free** (the panel polls it while an
install holds the card lock):
```json
{"active": true, "phase": "waiting_card",
"attempts": [{"ts": 1760000000.0, "iccid": "8970119000004600098",
"preset": "Batch 0002", "result": "installed",
"duration_ms": 41230, "final_cntr": "0000000005",
"load_file_aid": "F0414C46416101",
"module_aid": "F0414C4641610101", "nv_delta": 3120}],
"attempts_total": 3,
"current": null, "notice": "Card 8970119000004600098 installed.",
"counts": {"installed": 2, "failed": 1, "skipped": 0},
"started": 1760000000.0, "stopped": null, "progress": null}
```
`phase` is `idle` (no run), `waiting_card`, `installing`, `stopping`,
`stopped` or `finished`. A failed attempt carries `failed_step` (e.g.
`LOAD (3/7)`), `por_sw` (the card's own status word), `por_status`,
`por_error` and `sw`; a skipped one carries `reason`. `current` names the
card being installed (`{iccid, preset, started}`) and `progress` mirrors the
RAM progress payload while a chain runs. The report keeps the last 500
attempts (`attempts_total` counts them all).
- `POST /api/ram-batch/stop` — sets the stop flag: a running install finishes
first (an interrupted LOAD chain can leave a half-loaded package), then the
run ends. It touches no card state, so it answers while the card lock is
held (a `_CARD_FREE_POST` exception); a repeated stop is idempotent, `409`
when no run exists.
- `POST /api/ram-batch/clear` — drops the finished run's report (`409` while a
batch is active).
**Per-card resolution.** Every equipped card's **preset is matched by
ICCID**; the preset's ISD role TAR supplies the keyset (v3.26.0), the keys and
the counter seed (v3.20.0) — the same rules as `/api/ram-install`. A card
with no readable ICCID, no matching preset or no usable ISD keyset is recorded
as `skipped` with its reason and the batch keeps waiting. A card is attempted
**once per run** (any outcome): re-equipping it is recorded as
`already processed in this batch (<outcome>)`, never installed again —
restart the batch to retry it. Failures never stop the run and are never
retried automatically.
`/api/status` carries a small summary in `ram_batch` — the detailed state
**minus the attempts list** (`{active, phase, current, notice,
attempts_total, counts, started, stopped}`), so the panel's headline (the
phase, the current card, the counts) rides the existing 2 s status poll; the
attempts report is fetched from `/api/ram-batch/status` only when
`attempts_total` (or the active flag) changed.
### `POST /api/ram-install-app`
Run the single **INSTALL [for install]** (or **INSTALL [for make selectable]**) APDU for an *already loaded* package — one APDU, so the install parameters can be iterated without deleting and re-loading the `.cap`. The APDU builders are shared with `/api/ram-install` (`_cap_install_apdu` / `_cap_make_selectable_apdu`; both case 3 — no trailing `Le`, which the card's SCP80 layer counts as a phantom command) and the step verdict/counter logic is the same (`final_cntr` is returned on success **and** on failure).