fix(r2): the heid code-review panel (round "Wren", 4/4) — triaged and folded

Code fixes:
- The narrow-screen fold was specified and never built (4/4). The tray and
  notes are now closed <details> in the aside; above 1000px CSS alone
  (::details-content) shows them and hides the summary. There is no
  script. Browser-tested at 390 and 1400, JS on and off.
- The lightbox gated on parsed board rows, not page identity (3/4). It now
  uses is_board, the lesson the bench panel already carried.
- wants_json returned True at the first good entry, so a malformed later
  entry was never read (3/4). It now parses every entry first; any error
  is False.
- One flag predicate, flagged_targets. It serves the Desk count, the tray,
  the filmstrip, the tape and the review button. An unreadable flag entry
  counts nowhere.
- The header's open count and lifetime line, and the no-set marks panel,
  are now regions (they were stale after an in-place answer).
- Inline group headers render only when every group is one contiguous run.
  Interleaved directories no longer reprint or misfile headers.
- A booth held unreadable has no open_since, even with a readable pick
  beside the damage.
- The swap marks an absent region is-stale instead of leaving it looking
  current. It carries disclosure state (except the sent form's). The
  failure message is readable for 0.9 s before the reload.

Contract amended where the code was right and the text was not: the
wants_json and record_seen signatures, landed_at's three refinements, the
group position being ring-based, the end of the set offering every other
open pick, the Space-key player exception, and the fold mechanism.

New tests cover the parse order; a board with media; the header region; the
no-set panel; interleaved groups; mixed damage; the flag predicate; the
review recording .viewed; the fold at two widths with JS on and off; the
status message before the reload; a lost response after a landed write
(exactly one note); a stale absent region; stage node identity across a
swap; and F with a radio focused. The lost-response and stale tests turn
red under their mutations. 724 passed.
This commit is contained in:
vh
2026-09-23 09:41:18 -07:00
parent 881c7f5df3
commit fa5d46443d
7 changed files with 404 additions and 41 deletions
+48 -12
View File
@@ -419,21 +419,48 @@ def wants_json(accept: str | None) -> bool:
"""
if not accept:
return False
# EVERY entry is parsed before anything is decided: returning True at the
# first good JSON entry meant a malformed one after it was never read, so
# `application/json, application/json;q=broken` was a 204 and the same pair
# reversed a 303 (heid code-review, 3/4). One unparseable entry anywhere
# makes the whole header False.
wanted = False
try:
for entry in accept.split(","):
mtype, *params = entry.split(";")
if mtype.strip().lower() != "application/json":
continue
q = 1.0
for param in params:
key, _, value = param.partition("=")
if key.strip().lower() == "q":
q = float(value.strip())
if q > 0:
return True
if mtype.strip().lower() == "application/json" and q > 0:
wanted = True
except ValueError:
return False
return False
return wanted
def flagged_targets(marks: Sequence[Mark]) -> set[str]:
"""THE flag predicate (R2): the items carrying a READABLE flag mark. The
Desk count, the tray, the filmstrip, the tape and the review button all
read this, so they cannot disagree about one item. An unreadable flag
entry is judgment nobody can see, and counts nowhere."""
return {m.target for m in marks
if m.shape == "flag" and m.error is None and m.target}
def _contiguous(keys: Sequence[str | None]) -> bool:
"""True when each non-None key occupies ONE unbroken run of the sequence."""
seen: set[str] = set()
prev = object()
for k in keys:
if k != prev:
if k is not None and k in seen:
return False
if k is not None:
seen.add(k)
prev = k
return True
# The Desk shows this many bookmarks and links to the board for the rest.
@@ -615,8 +642,12 @@ def list_booths(data_dir: Path, ttl_seconds: float, now: float | None = None) ->
"expires_in": max(0.0, ttl_seconds - (now - mtime)),
"mtime": mtime,
# ---- R2 C4, the Desk. All from the pass above; no second read.
"open_since": min(stamps) if stamps else None,
"flags": sum(1 for m in marks if m.shape == "flag"),
# A booth held for UNREADABLE marks has no `open_since`, even if
# a readable pick sits beside the damage: it sorts after every
# dated question, because the damage is what needs fixing.
"open_since": (min(stamps) if stamps and hold != HOLD_UNREADABLE
else None),
"flags": len(flagged_targets(marks)),
# Two clocks, named apart (INV-5): `mtime` is activity,
# `landed_at` is content. "New since you looked" reads only the
# second, so a flag or a view never makes a booth look new.
@@ -1142,8 +1173,14 @@ def create_app(
# R2 C5: the flag tray — the flagged items in SET order, i.e.
# by ordinal, over the full gallery (a filter hides tiles, not
# judgments). Order is total with no tie-break: rels are unique.
"tray": [it for it in gallery if it["name"] in {
m.target for m in marks if m.shape == "flag" and m.error is None}],
# Inline group headers only when every group is ONE contiguous
# run in the rendered order. Groups come from basenames and the
# order from full paths, so they can interleave (d1/aa, d1/bb,
# d2/aa); a header then either repeats or files an item under
# the wrong group. The rail's jump links do not depend on this.
"inline_groups": bool(rail["groups"]) and _contiguous(
[it["group"] for it in shown]),
"tray": [it for it in gallery if it["name"] in flagged_targets(marks)],
"ord_width": len(str(len(gallery))),
"uploaded": (booth / UPLOAD_MARKER).exists(),
# The same provenance line the index card carries. Deliberate:
@@ -1703,7 +1740,7 @@ def create_app(
# notes and the flag state travel to full size, which is the size at
# which the judgment is actually being made.
"marks": item_marks,
"flagged": any(m.shape == "flag" for m in item_marks),
"flagged": f in flagged_targets(marks),
}
if item is not None and item.kind in REVIEW_KINDS:
@@ -1718,8 +1755,7 @@ def create_app(
if len(ring) > 1:
prev_url = quote(ring[(pos - 1) % len(ring)], safe="/")
next_url = quote(ring[(pos + 1) % len(ring)], safe="/")
flagged_rels = {m.target for m in marks
if m.shape == "flag" and m.error is None}
flagged_rels = flagged_targets(marks)
# recorded above, before this read: the current item counts as seen
seen = read_seen(booth) & set(ring)
film = [{"name": r, "url": by_rel[r].url, "ordinal": by_rel[r].ordinal,