feat(booth): asks — a multiple-choice question a session poses in a booth, answered by the operator as a radio form + notes, written back as an answer sidecar
- booth/asks.py (stdlib): <stem>.ask.json question / <stem>.answer.json answer; normalise+validate, atomic write, list with answer folded in, broken asks surfaced not hidden - POST /b/<name>/answer: validates choice against the ask (400), unknown stem 404, re-answer overwrites - booth.html asks panel above the gallery; amber open / green answered; JS-off form POST; index card + booth header badge for open asks - CLI: booth ask / asks / answer [--wait [SECS]]; remote sessions poll <stem>.answer.json over HTTP - ask/answer files excluded from gallery items and item counts; 23 tests; v0.1.9
This commit is contained in:
@@ -88,6 +88,60 @@ Deliberately **not** a database. The board is a markdown file — editable with
|
||||
any editor, greppable, and trivially prunable by hand, which is the whole point
|
||||
of the Booth's filesystem-is-the-state model.
|
||||
|
||||
## Asks — let the operator pick one of N, and read the pick back
|
||||
|
||||
The one **interactive** primitive. A session needs a human decision — which
|
||||
render wins, which plan, go/no-go — and wants to act on it without a chat
|
||||
round-trip. Drop a question in a booth; the page renders it as a radio form
|
||||
with a notes field; the operator's submit writes an **answer sidecar** the
|
||||
session reads. Filesystem is still the state:
|
||||
|
||||
```
|
||||
<booth>/<stem>.ask.json the question (a session writes it)
|
||||
<booth>/<stem>.answer.json the answer (the web UI writes it, atomically)
|
||||
```
|
||||
|
||||
```bash
|
||||
# On nh3-dev — pose, then block until answered (default 1h), then act on it:
|
||||
booth ask r18-ab winner "Which render wins?" "A — baseline" "B — cudaMallocAsync"
|
||||
booth answer r18-ab winner --wait # prints the answer JSON when it lands
|
||||
booth answer r18-ab winner # non-blocking: exit 1 while unanswered
|
||||
booth asks r18-ab # list a booth's asks + state
|
||||
|
||||
# Options can carry an id + detail line instead of a bare label — write the
|
||||
# JSON yourself (booth.asks.write_ask validates the same way):
|
||||
cat > ~/booth-data/r18-ab/plan.ask.json <<'EOF'
|
||||
{"prompt": "Ship which?",
|
||||
"options": [{"id": "a", "label": "Plan A", "detail": "smaller diff, no migration"},
|
||||
{"id": "b", "label": "Plan B", "detail": "cleaner, needs the DB change"}],
|
||||
"notes": true, "notes_label": "why / conditions"}
|
||||
EOF
|
||||
|
||||
# From another host: rsync the ask in, then poll the sidecar over HTTP:
|
||||
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",
|
||||
"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.
|
||||
|
||||
Rules of the primitive:
|
||||
|
||||
- **Radio, one pick.** ≥ 2 options, ≤ 40. No multi-select (not yet asked for).
|
||||
- **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
|
||||
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
|
||||
validate, the page says why, so a session never thinks it posted a question
|
||||
the operator cannot see.
|
||||
- Ask/answer files are not gallery items and do not count toward the booth's
|
||||
item count; they render as the panel above the gallery. Answering bumps the
|
||||
booth's mtime, so it lives another TTL — the session has 24h to read it.
|
||||
- Works with JavaScript off (plain form POST). No auth, same as everything here.
|
||||
|
||||
## Upload for pickup
|
||||
|
||||
The reverse direction — put files in through the web, pick them up by id:
|
||||
@@ -136,6 +190,7 @@ to a safe basename (no path traversal).
|
||||
| `GET /b/<name>/` | A booth (its `index.html`, else auto-gallery) |
|
||||
| `GET /b/<name>/<file>` | Serve a file out of the booth |
|
||||
| `POST /upload` | Upload files → new pickup booth; 303-redirects to `/b/<id>/` (id in `Location`) |
|
||||
| `POST /b/<name>/answer` | Answer an ask (form fields `ask` = stem, `choice` = option id, `notes`); writes `<stem>.answer.json`, 303 back to the booth |
|
||||
| `POST /b/<name>/delete` | Wipe a booth (the UI's "Wipe now" button) |
|
||||
| `POST /b/<name>/keep` | Pin a booth — exempt from the sweep |
|
||||
| `POST /b/<name>/unkeep` | Release the pin (the UI's "release" button on kept cards) |
|
||||
|
||||
Reference in New Issue
Block a user