fix(booth): asks were invisible in a booth serving its own index.html

A custom index.html is returned verbatim, so booth.html's asks panel never
rendered there — a valid ask (emmie-anchor/anchor.ask.json) was listed by the
CLI and shown nowhere, with nothing to say so.

- panel extracted to _asks.html; new GET /b/<name>/asks standalone page
- verbatim pages get an amber '? N open asks' chip beside the back chip
- POST /answer honours back=asks so answering returns to that page
- single-question asks now keep an optional 'title' (was silently dropped)
- README + routes table; 8 regression tests; v0.1.12
This commit is contained in:
vh
2026-09-09 10:39:18 -07:00
parent 047749c3cf
commit f99faabb80
9 changed files with 244 additions and 82 deletions
+51 -5
View File
@@ -393,6 +393,29 @@ _BACK_CHIP = (
"@media print{.booth-nav-home{display:none}}</style>"
)
# A booth's own index.html is served VERBATIM, so the asks panel — which lives in
# the auto-gallery template — can never appear on it. Without this chip an ask
# posted into a custom-report booth is INVISIBLE to the operator with nothing to
# say so (found 2026-09-09 on `emmie-anchor`: valid ask, CLI listed it, page
# showed nothing). Same injection mechanism as the back chip; it links to the
# standalone /asks page, which renders the real forms.
def asks_chip(name: str, open_count: int) -> str:
if open_count < 1:
return ""
label = f"? {open_count} open ask" + ("" if open_count == 1 else "s")
return (
f'<a href="/b/{quote(name, safe="")}/asks" class="booth-nav-asks">{label}</a>'
"<style>.booth-nav-asks{position:fixed;top:0;right:7.2rem;z-index:2147483647;"
"display:inline-block;margin:.6rem;padding:.34rem .72rem;"
"font:700 13px/1.25 ui-sans-serif,system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;"
"color:#171a23;text-decoration:none;letter-spacing:.01em;"
"background:#ffe14e;border:1px solid #ffe14e;border-radius:8px;"
"box-shadow:0 2px 10px rgba(0,0,0,.35);transition:filter .18s}"
".booth-nav-asks:hover{filter:brightness(1.08)}"
"@media print{.booth-nav-asks{display:none}}</style>"
)
WRAP_MAX_BYTES = 8 * 1024 * 1024 # above this, serve the verbatim page raw (unwrapped)
_ICON_RE = re.compile(r"<link\b[^>]*\brel\s*=\s*[\"']?[^\"'>]*icon", re.IGNORECASE)
@@ -417,7 +440,7 @@ def _insert_after(html: str, pattern: re.Pattern, snippet: str) -> tuple[str, bo
return html, False
def wrap_verbatim_html(html: str, favicon_link: str = FAVICON_LINK) -> str:
def wrap_verbatim_html(html: str, favicon_link: str = FAVICON_LINK, extra: str = "") -> str:
"""Inject a floating 'all booths' back-chip — and the Booth favicon, if the page
declares none — into a booth's verbatim index.html, without altering the page's
rendered content.
@@ -443,12 +466,13 @@ def wrap_verbatim_html(html: str, favicon_link: str = FAVICON_LINK) -> str:
else:
html = favicon_link + html # bare fragment, no doctype: safe to prepend
chips = _BACK_CHIP + (extra or "")
for pat in (_BODY_CLOSE_RE, _HTML_CLOSE_RE):
html, done = _insert_before(html, pat, _BACK_CHIP)
html, done = _insert_before(html, pat, chips)
if done:
break
else:
html = html + _BACK_CHIP # no </body>/</html>: append to the end
html = html + chips # no </body>/</html>: append to the end
return html
@@ -612,8 +636,12 @@ def create_app(
# a pathological large file falls back to serving raw, unwrapped.
try:
if own_index.stat().st_size <= WRAP_MAX_BYTES:
open_asks = sum(1 for a in list_asks(booth) if a["answer"] is None and not a["error"])
return HTMLResponse(
wrap_verbatim_html(own_index.read_text(encoding="utf-8", errors="replace"))
wrap_verbatim_html(
own_index.read_text(encoding="utf-8", errors="replace"),
extra=asks_chip(name, open_asks),
)
)
except OSError:
pass
@@ -681,7 +709,25 @@ def create_app(
write_answer(booth, ask, form.get("choice"), form.get("notes", ""), who=who)
except AskError as exc:
raise HTTPException(status_code=400, detail=str(exc))
return RedirectResponse(url=f"/b/{quote(name, safe='')}/#ask-{quote(ask, safe='')}", status_code=303)
# Land where the form was: the standalone /asks page for a verbatim booth
# (its own index.html cannot show the recorded answer), else the booth.
base = f"/b/{quote(name, safe='')}/"
if form.get("back") == "asks":
base = f"/b/{quote(name, safe='')}/asks"
return RedirectResponse(url=f"{base}#ask-{quote(ask, safe='')}", status_code=303)
@app.get("/b/{name}/asks", response_class=HTMLResponse)
def booth_asks_page(request: Request, name: str):
"""The asks panel on its own page. Reachable from any booth, and the ONLY
place a verbatim-index.html booth can show its asks — that page is served
untouched by design, so the inline panel never renders there."""
booth = resolve_booth(name)
return templates.TemplateResponse(
request,
"asks.html",
{**base_ctx, "name": name, "name_url": quote(name, safe=""),
"asks": list_asks(booth), "asks_page": True},
)
@app.get("/b/{name}/view", response_class=HTMLResponse)
def booth_view_file(request: Request, name: str, f: str):