Files
booth/persistent-memory.d/2026-09-27-one-submit-saves-every-ask.md
T
vh 50bfc7b4ec fix(asks): one submit saves every ask on the page
Operator report (via infra-ops): on a page with several asks, a submit
saved only the pressed one and the reload wiped the rest. Confirmed on
auk-audition: one POST at 15:02:23 saved the last ask on the page, then a
400 from the submit of an ask the reload had just blanked.

Client-side on both surfaces; /answer is unchanged. A submit on a pick
form, while another pick form on the page holds unsent input, sends every
changed ("dirty") pick form: one POST each, to its own action, with
Accept: application/json, in document order. A refusal stops nothing, and
untouched forms are never re-sent. With no other dirty form, a submit is
exactly what it was.

- embed.js (verbatim reports): reloads only when nothing was refused and
  nothing of ours is dirty. Otherwise a server-rendered status line in the
  submit block says what did not save, and input stays. A form the server
  took gets a new baseline. A press during the flight is ignored.
- base.html (marks page, lightbox, review rail): one refresh in place. A
  batch never reloads. Only forms the server took count as sent. In-flight
  state and "just sent" are keyed by form identity (formKey) plus the fields
  at the press, not the DOM node.

Two heid bug-hunt rounds: a four-arm panel on the first cut, then Hulda
alone on the fold. Ten findings reproduced red in a browser before their
fixes. Contracts: U3 "Submitting several asks at once" + INV-8, R2 C3
steps 2, 3 and 3a. Mutation tables u3_submit_all (15) and r2_submit_all
(11), all proved. Suite 928 -> 951.
2026-09-27 17:05:11 -07:00

103 lines
6.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 2026-09-27 — One submit saves every ask on the page
**The report.** Prime to infra-ops, relayed to booth-dev (althing thread
`01M3JED397G1SZH7580PCXNVVA`): "submitting a question should go through and
submit ALL answers. As it is, I go through, submit a question and it only
submits the last one and clears out the top ones."
**Confirmed against live data before any code, and it matched to the second.**
`auk-audition`: three single-question picks, no anchors, so all three in the
embed tail in `(created, id)` order. The access log: one POST at 15:02:23 (303)
that saved `auk-emotion`, the LAST pick on the page; the reload; a POST at
15:02:27 that 400'd (the submit of a pick the reload had just blanked — the
browser showed a raw `{"detail": ...}` page); then `auk-clone` at 15:04:45 and
`auk-events` at 15:05:02, answered one at a time. infra-ops' reading of the
code was right. `vastblue-site-visit-2026-09-29` carries 14 picks and would
have hit it next.
**The cause is not the server.** One pick = one `<form>` = one POST to
`/answer`, and every POST did what `/answer` promises. The page offered one
button per form and no way to send them together. Two surfaces, two machineries:
- **Verbatim report (embed.js).** Plain form POST + 303 reload. The reload
wiped every unsent pick.
- **Booth pages (base.html's in-place script: marks page, lightbox verdict
aside, review rail).** R2 C3 already CARRIED unsent picks across a swap, so
they survived — but were never SAVED. And pressing a blank pick's submit was
a 400, whose failure path reloads and wipes them all.
**The shape (booth-dev's call; infra-ops said "the shape is your call").**
Client-side, both surfaces, no server change: a submit on a pick form, while
ANOTHER pick form on the page is dirty (a control differs from its
server-rendered default), sends every dirty pick form — the pressed one only if
dirty — one POST each to the unchanged `/answer` with `Accept:
application/json` (the 204), serially in document order, a refusal stopping
none of the rest. With no other dirty form, nothing changes: the browser's own
POST (verbatim) or C3's one-form path (Booth pages).
- **Rejected: a server batch endpoint + one page-level form.** It would also
fix no-JS, but needs ask-scoped field names, a single `<form>` element placed
by embed.js, and rewires `formKey` identity on the Booth pages (one form
holding many `ask` fields). Large blast radius for a no-JS verbatim path that
does not exist (U3's named cost). Atomic all-or-nothing was also the WRONG
semantics: one stale pick would refuse the rest — the 2026-09-09
partial-answer ruling's reasoning, one level up.
- **Untouched picks are not re-sent**, the pressed one included: re-sending
re-dates an answer nobody gave.
- **A refusal never clears what was entered, on either surface** (the first
cut had the Booth batch take C3's say-and-reload path; heid's panel caught
it, see below). Verbatim: no reload while anything of ours is dirty; the
pressed form's server-rendered, empty, hidden `.bk-ask-status` line says what
did not save. Booth pages: refresh in place with only the forms the server
took counted as sent, so the refused pick and any draft carry by identity;
the status region names what did not save. C3's ONE-form failure path
(step 4, say and reload) is untouched — not this change's surface, and it
still loses drafts on a refusal (named, not fixed).
**The heid bug-hunt panel (4/4 arms, thread `01M3JFDM1AWT2TCKS6E9NJM9G4`) was
the gate that paid.** All four landed on the same blind spot: the batch reads
every form AT THE PRESS but the page stays live through the flight, and every
guard protecting that window (`sending`, `__busy`, `held`) SURVIVED mutation,
because no test pressed or edited inside a flight. Six of its rows reproduced
RED in a browser before any fix: a successful embed batch reloading away a pick
made mid-flight (S1); the Booth batch's refusal reload (S2); saved forms staying
dirty so a retry re-dated them (S3); in-flight marked on DOM nodes a queued
swap replaced (S5 — now keyed by `formKey` identity via `flightKey`); a press
in flight falling through to a native POST (embed) or a blank 400 (Booth) (S6).
The trick that made them testable: hold every POST's reply 700ms in the CLIENT
(`_HOLD_POSTS`, the `_HOLD_FIRST_REFRESH` pattern) so the window is wide enough
to act in on purpose. S7 (no ask dedupe) rejected as unreachable; S8–S11
accepted with reasons in the fold reply (`01M3JHYKA0GSJG4F836J1Z6TJS`).
**Lesson: a feature that opens an async window needs a test that acts inside
it; a green suite of single presses says nothing about the window.**
**Round two: a single cold Hulda arm on the FOLDED tree found six more**
(thread `01M3JHYKCPXFA34XMRCEW4P3DJ`), all in how the fold's own rules
interacted — and Heid's primed second voice, reading only the fold's delta,
cleared two of them wrongly and retracted. Four reproduced RED: a sent pick
changed mid-flight came back as the saved copy (#1); a note queued behind a
flag carried back as a draft because "just sent" was a DOM-node test (#3);
a batch whose page GET failed reloaded drafts away (#4); the embed reloaded
over a refusal the operator had set back to its first value (#6). Fix:
"just sent" = `flightKey` identity + the form's serialization at the press
(`sentSet`), a batch never reloads, and a refusal blocks the embed reload on
its own. #2 was a message fix (a withdrawn pick's input has nowhere to go);
#5 accepted (needs a refused POST AND a failed GET; costs a re-date of
identical content). **A cold read of the whole diff beat a primed read of the
delta** — worth remembering before scoping a re-review to "just the fold".
**Contracts amended in the same commit:** U3 "Submitting several asks at once"
+ INV-8; R2 C3 steps 3 and 3a. **Mutation tables:** `tests/mutations/u3_submit_all.toml`
(15 rows) and `tests/mutations/r2_submit_all.toml` (11 rows), all proved; the
r2_flow row anchored on `form.__busy` moved to `pending[flightKey(form)]`.
**Tests:** 23 new browser tests plus two tightened (the author-form test now
wears our id prefix; the one-form failure test now asserts the reload it names).
Suite 928 → 951.
The `[a3]` case reproduced the live log exactly (only a3 saved).
**Not done, named:** no no-JS batch on the Booth pages (each plain form still
saves one pick with scripts off — the no-JS path is unchanged, as asked); a
verbatim batch that keeps the page leaves the saved picks' tags stale until a
reload, and the message says so; C3's one-form refusal still reloads drafts
away; Hulda #5 (above) accepted.