feat(booth): multi-question asks — a questions list renders one form with a radio group per question and lands as one answer sidecar keyed by question

- asks.py: single {prompt, options} and multi {title, questions:[{key, prompt, options, notes?}]} both normalise to questions[]; per-question notes; every question required on submit
- /answer reads choice.<key> / notes.<key> / notes for multi; single shape unchanged
- booth asks prints per-question picks; README + CLI header; install step symlinks the CLI to ~/.local/bin; v0.1.10; 135 tests
This commit is contained in:
vh
2026-09-09 09:20:35 -07:00
parent 3fe01225a9
commit c85a700141
8 changed files with 355 additions and 69 deletions
+28 -4
View File
@@ -121,15 +121,38 @@ EOF
curl -sf http://10.100.10.50:8090/b/r18-ab/winner.answer.json # 404 until answered
```
The answer: `{"stem", "prompt", "choice", "choice_index", "label", "notes",
**Several questions, one form.** Give the ask a `questions` list instead of
`prompt`+`options`; the page renders one form with a radio group per question
and a single submit, every question required. Per-question `notes: true` adds
a small text field under that question; the form-level `notes` stays one field
for the whole ask. The answer is keyed by question:
```bash
cat > ~/booth-data/r18-ab/batch.ask.json <<'EOF'
{"title": "R18 batch review",
"questions": [
{"key": "r1", "prompt": "Render 1 — keep?", "options": ["keep", "drop"], "notes": true},
{"key": "r2", "prompt": "Render 2 — keep?", "options": ["keep", "drop"]},
{"key": "seed", "prompt": "Reseed the batch?", "options": ["yes", "no"]}],
"notes": true, "notes_label": "anything else"}
EOF
# -> batch.answer.json: {"stem", "title", "answers": {"r1": {"prompt", "choice",
# "choice_index", "label", "notes"}, "r2": {...}, "seed": {...}}, "notes", "answered_at", "answered_by"}
```
The single-question answer: `{"stem", "prompt", "choice", "choice_index", "label", "notes",
"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` (form fields `ask`, `choice`, `notes`)
is what the form submits; a bad choice is a 400, an unknown stem a 404.
client address. `POST /b/<name>/answer` is what the form submits — fields `ask` plus
`choice` / `notes` (single) or `choice.<key>` / `notes.<key>` / `notes` (multi);
a missing or bad choice is a 400, an unknown stem a 404.
Rules of the primitive:
- **Radio, one pick.** ≥ 2 options, ≤ 40. No multi-select (not yet asked for).
- **Radio, one pick per question.** ≥ 2 options, ≤ 40 per question, ≤ 30
questions per ask. No multi-select checkboxes (not yet asked for). Many asks
per booth are fine — each is its own form and its own sidecar; use
`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.
- **Open asks are flagged** — an amber `? N asks` badge on the index card and in
@@ -293,6 +316,7 @@ Config is env in the unit (`booth.service`):
```bash
cd services/booth
uv venv && uv pip install fastapi "uvicorn[standard]" jinja2 python-multipart # runtime deps
ln -sfn "$PWD/scripts/booth" ~/.local/bin/booth # the `booth` CLI on PATH (nh3-dev has this)
cp booth.service ~/.config/systemd/user/booth.service
systemctl --user daemon-reload && systemctl --user enable --now booth.service
```