fix(booth): a partial ask answer is recorded, not refused
Operator: the form failed when a question was left blank. Refusing the whole submission over one blank threw away the picks that were made, and the HTML `required` on the radios blocked it in the browser before the server saw it. - answered questions recorded; blank ones land in `unanswered`; `complete` says whether the set is finished; a blank question carrying a note keeps the note - `required` dropped from both templates so the browser cannot block a partial - refused only when there is no pick anywhere AND no notes (a 400 — that would flip an open ask to answered with no decision recorded); a choice outside the option list is still an error - new ◐ partial state with an n/N count; skipped questions render as skipped - README + global CLAUDE.md tell reading sessions to check `complete` - 154 tests; v0.1.15
This commit is contained in:
@@ -141,8 +141,14 @@ EOF
|
||||
# "choice_index", "label", "notes"}, "r2": {...}, "seed": {...}}, "notes", "answered_at", "answered_by"}
|
||||
```
|
||||
|
||||
Both shapes also carry **`unanswered`** (the question keys left blank; `[null]`
|
||||
for a blank single-question ask) and **`complete`** (false until every question
|
||||
has a pick). A reading session should check `complete` before acting on a
|
||||
multi-question answer, and treat a key in `unanswered` as "not decided", never
|
||||
as "declined".
|
||||
|
||||
The single-question answer: `{"stem", "prompt", "choice", "choice_index", "label", "notes",
|
||||
"answered_at", "answered_by"}` — `choice` is the option id (the label itself
|
||||
"unanswered", "complete", "answered_at", "answered_by"}` — `choice` is the option id (the label itself
|
||||
for string options), `choice_index` its 0-based position, `answered_by` the
|
||||
client address. `POST /b/<name>/answer` is what the form submits — fields `ask` plus
|
||||
`choice` / `notes` (single) or `choice.<key>` / `notes.<key>` / `notes` (multi);
|
||||
@@ -156,6 +162,19 @@ Rules of the primitive:
|
||||
`questions` when the picks belong together and should land as one answer.
|
||||
- **Re-answering overwrites.** The sidecar is the *current* answer, not a log.
|
||||
The page shows the recorded answer with a collapsed *change answer* form.
|
||||
- **Blanks are legal — a partial answer is recorded, not refused.** Leaving a
|
||||
question alone is a real outcome ("none of these", "not listened to yet"), and
|
||||
refusing the whole submission over one blank threw away the picks that WERE
|
||||
made. So every answered question is recorded, every blank one lands in
|
||||
`unanswered`, and `complete` says whether the set is finished. The radios carry
|
||||
no HTML `required`, so the browser does not block the submit either. A question
|
||||
left blank but carrying a note keeps the note (`choice: null`). The one refusal
|
||||
is a submission with **no pick anywhere and no notes** — a 400, because it would
|
||||
flip an open ask to "answered" while recording no decision, which is worse for
|
||||
the reading session than leaving it open. A choice that is not in the option
|
||||
list is still an error: that is a broken form, not a skipped question.
|
||||
Partially-answered asks show as `◐ partial` with an `n/N` count; re-submitting
|
||||
fills in the rest.
|
||||
- **Open asks are flagged** — an amber `? N asks` badge on the index card and in
|
||||
the booth header — so a waiting question is visible from the front page.
|
||||
- **A broken ask is shown as broken**, not hidden: if the JSON does not
|
||||
|
||||
Reference in New Issue
Block a user