{% extends "base.html" %} {% from "_provenance.html" import provenance %} {% from "_lifetime.html" import lifetime %} {% from "_dates.html" import dates %} {# The blur toggle, defined ONCE. There are three item branches in this file (doc / media / other) and the first cut of this feature patched only one of them, so docs rendered with no control at all. A macro makes "patched two of three" impossible rather than merely unlikely. #} {% macro blurtoggle(name_url, it, cls='') -%} {# Blurred only because the whole booth is (r2b D2b): say so, and offer no per-item un-blur — the booth flag would keep it blurred, so the control would do nothing visible. The header un-blurs the booth. #} {% if it.blurred and not it.blurred_self %} ◉ booth {% else %}
{% endif %} {%- endmacro %} {# The per-item MARK controls: flag (the operator pointing at this one) and a note field. Same macro discipline as blurtoggle above — three item branches, one definition. `marks` here is THIS item's marks, from item_marks. #} {% macro markcontrols(name_url, it, marks, cls='') -%} {# THE flag predicate (flagged_targets), shared with every other surface #} {% set flagged = it.name in flagged_set %}
{%- endmacro %} {# An item's notes, rendered BESIDE the artifact — the 2026-09-09 ruling that a judgment belongs with the thing it is about, applied to notes as well as picks. The add-field is a
so 270 tiles do not each carry an open textarea. #} {% macro marknotes(name_url, it, marks) -%} {% for m in marks if m.shape == 'note' %}
{{ m.text }}
{% endfor %}
+ note
{%- endmacro %} {# R2 C1: an item's number in the WHOLE set, zero-padded to the set's width so a column of them lines up. Width reads `all_items`, never the filtered list: a filter must not change how a number is written any more than which. #} {% macro ordinal(it) -%} #{{ "%0*d"|format((all_items|length|string|length), it.ordinal) }} {%- endmacro %} {% block title %}{{ name }} · The Booth{% endblock %} {% block html_attrs %} data-booth="{{ name }}"{% endblock %} {% block content %}
‹ all booths {# The manifest's TITLE is the display name; the directory name stays visible beside it because that is the identity the operator navigates by and refers to positionally, and losing it would be losing the thing the URL says. Index cards keep the directory name alone for the same reason. #} {% if manifest and not manifest.error and manifest.title and manifest.title != name %}

{{ manifest.title }} {{ name }}

{% else %}

{{ name }}

{% endif %} {# The open count and the lifetime line depend on marks, so they are a region (R2 C3): answering the last pick in place must not leave "1 open" behind. #} {% if uploaded %}⬆ pickup {% endif %}{% if board %}{{ board|length }} link{{ '' if board|length == 1 else 's' }}{% if items %} · {{ items|length }} file{{ '' if items|length == 1 else 's' }}{% endif %}{{ dates(created_at, landed_at, now) }} · {{ lifetime(kept, hold, expires_in) }}{% else %}{% if marks_open %}{{ marks_open }} open · {% endif %}{{ items|length }} item{{ '' if items|length == 1 else 's' }}{{ dates(created_at, landed_at, now) }} · {{ lifetime(kept, hold, expires_in) }}{% endif %} {% if items %}⬇ zip{% endif %} {{ provenance(manifest) }} {# A durable multi-writer board gets no one-click wipe — same rule as the kept lane on the index. Remove rows with the per-row ×, or release the board from the index and wipe it from there. #} {# Promote or release without going back to the index. `next` keeps you on this page instead of bouncing you to /. #} {% if kept %}
{% else %}
{% endif %} {# r2b D2b + D2: the booth-wide blur controls, outside every data-region. The fog is server state for every viewer and a plain form (works with scripts off); its label says what IS. Reveal all lifts it for this tab only, and is markup only when something here is blurred. A BOARD gets them too when it holds files: only the one-click wipe is board-suppressed, and an item's "◉ booth" label points here. #} {% if all_items %} {# The fog form IS a region: its label is server state, so an in-place save refreshes it with everything else (a fog set elsewhere since this page loaded would otherwise leave it saying "blur booth"). Reveal all is not: its state lives in this tab, and a swap must never reset it. #}
{% if all_items | selectattr('blurred') | list %}{% endif %} {% endif %} {% if not board %}
{% endif %}
{% if uploaded %}
📦 Pickup {{ name }} — download files below, or on nh3-dev grab ~/booth-data/{{ name }}/
{% endif %} {# The marks panel: the session's questions, the operator's notes, and the way back to the flagged items. Always rendered on a gallery booth — the add-note field is a control, not a result, so it has to be there before the first mark exists. #} {# `marks or not board`: the standing link board renders as a board rather than a gallery, and the add-note control would be noise on it — but the suppression was unconditional, so a pick declared on a booth that happens to carry a links.md had no form to answer it and nothing said so. #} {# R2 C5: on a GALLERY booth the panel moves into the verdict aside beside the set (below). It renders up here only where there is no set to sit beside — a board, or a booth with marks and nothing to show. #} {# `is_board`, not `board`: PAGE IDENTITY, not page content — the lesson the bench panel already learned. `board` is the parsed rows, empty for a links.md with none, and a board with an image in it must still be a board. #} {% set lightbox = all_items and not is_board %} {% if (marks or not board) and not lightbox %}
{% include "_marks.html" %}
{% endif %} {# THE BENCH REGISTRY — BLOCK LEVEL, and that placement is load-bearing. This
spent one commit nested inside the `` of the booth header, because the insertion matched the FIRST `{% if board %}` in the file rather than the block-level one. A
inside a is invalid HTML: the parser closes the span implicitly and hoists the div out, orphaning the rest of the sub-line. Three of four cold bug-hunt arms found it and the seat confirmed it in the live document by byte offset. Keep this block between the marks panel and the board form. #} {% if is_board %} {# THE BENCH REGISTRY. A bench is a running thing — jackdaw's current bench, talk's current bench, the things that get promoted to Homepage when they are fully deployed. NOT a booth (a booth announces itself and is swept) and NOT a bookmark (a repo page, a model card — those stay on the board below). Identity is the NORMALIZED URL, so re-announcing a bench updates its row instead of appending a fifth. `talk` was on the board five times. ORDER: state (live → promoted → retired), then name, then id as a total tie-break so two benches sharing a name cannot swap between renders. The href is `b.url` — the URL AS POSTED — never `b.id`. The id is normalized for identity; a server that cares about a trailing slash or a case-sensitive path would 404 on it. #}
{{ benches|length }} bench{{ '' if benches|length == 1 else 'es' }} a running thing, registered · re-posting updates the row
{% if benches_error %} {# DAMAGED AND ABSENT MUST NOT RENDER THE SAME. Only one of them needs a human, and the v0.2.2 outage was learned by treating them alike. #}
the bench registry could not be read: {{ benches_error }}
{% elif not benches %}
no benches registered yet — booth bench add <url> <name>
{% endif %} {% for b in benches %}
{{ b.state }}
{{ b.name or b.url }}
{{ b.url }}
{% if b.owner %}{{ b.owner }}{% endif %} {# The date it was REGISTERED, not the date it was last touched: `added` survives re-registration and `updated` does not, so `added` is the one that answers "how long has this been around". #} {% if b.added %}{{ b.added[:10] }}{% endif %}
{% for s in ("live", "promoted", "retired") %} {% if s != b.state %} {% endif %} {% endfor %}
{% endfor %}
{% endif %} {% if board %} {# THE STANDING LINK BOARD. Every agent session on the fleet appends here, so this is the one booth where the useful granularity is the ROW, not the folder. Rendered as real UI rather than a markdown blob so a dead link can be removed without hand-editing the file — and so provenance (who posted it, when) is readable at a glance, which is the whole reason a bare URL three days old is useless. ORDER: pinned rows first, then newest-first (order_for_display). Pin a row with the ★ so the ones you care about stop scrolling off the bottom. ONE
, not one-per-row: checkboxes drive the bulk delete, while the per-row × and ★ are submit buttons with their own `formaction`. That keeps all three actions in a single form (nested forms are invalid HTML) AND lets every one work with JS off — JS only adds select-all and the live count. Every action posts a CONTENT ID, never a row number: another session can append between this page rendering and a click, and an index would then hit a neighbour. An id matches the row the operator saw, or nothing. #} {% set pinned_n = board | selectattr('pinned') | list | length %}
{{ board|length }} link{{ '' if board|length == 1 else 's' }}{% if pinned_n %} · {{ pinned_n }} pinned{% endif %} pinned first · newest on top · ★ pins a row · tick rows to delete
{% for e in board %} {# DEAD: the row points at a booth that has been swept. 156 of 221 rows. MARKED, never removed — removal is the operator ticking the box and using the bulk control that was already here. #}
{% if e.safe %}{{ e.desc }} {%- else -%} {# Refused, not hidden: the operator should see that something was posted and that we would not link it. `is_safe_href` decides, in links.py. #} {{ e.desc }} unsafe link refused {%- endif %}
{{ e.url }}{% if e.dead %} booth is gone{% endif %}
{% if e.who %}{{ e.who }}{% endif %} {% if e.when %}{{ e.when }}{% endif %}
{% endfor %}
{% endif %} {# ⚠ THE RAIL IS GATED ON `all_items`, NOT `items`, AND THAT IS THE WHOLE POINT. `items` is the FILTERED list, so gating on it meant a valid filter with zero hits removed the rail, the filter links and the only way back to `all` — while the empty-booth branch below announced the booth was empty with `rail.total` still holding the real count. No recovery without editing the address bar, and it failed the same way with JavaScript off, on the surface the operator actually reviews on. Found by the heid bug-hunt panel (gróa, 2026-09-22), whose own note called it the finding most likely to bite users this week. #} {% if not all_items and not board and not marks %}
This booth is empty.
{% elif all_items %} {# THE LIGHTBOX (R2 C5). The verdict aside comes FIRST in the document and the set second: on a narrow screen that is the stacking the contract wants (the question above the work), and on a wide one the grid areas in base.html put the aside on the right. Placement, not order — nothing in an ordered collection moves. #} {% if lightbox %}