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.