fix(r2): the heid bug-hunt panel (round "Nyx", 4/4) — triaged and folded

In-place client (base.html):
- Saves are serialized: POST, re-fetch and swap complete before the next
  save starts, so an older snapshot can no longer land after a newer one.
- A form already queued or in flight ignores another submit; a
  double-click writes one note.
- Dirty controls (drafts, unsent radio choices) and disclosures carry by
  identity (form action + hidden ask/target/mark/f + name), not position.
- Any non-tile structural difference, or a page with no region to swap,
  reloads instead of patching.

Server and templates:
- .seen is a JSON array read without following links or blocking,
  regular files of at most 1 MiB only; malformed, nested-too-deep or
  planted markers read as nothing seen.
- landed_at reads symlinks by lstat and skips one unreadable entry
  instead of pinning the booth in "new".
- The Desk counts flags on current items only; orphan flags are listed
  under the tray with an unmark form.
- Agent-written bench and bookmark URLs link only when http(s).
- Audio and video tiles carry a review link.
- A rel the filesystem cannot represent is a 404, not a 500.
- A non-finite Accept q-value fails to parse.
- The standalone marks page has regions and updates in place.
- The review's next arrow sits at the edge at phone width.

Contract amended for each, plus an accepted-risks section (unlocked
.seen read-modify-write, a planted .viewed symlink, Item.ordinal with
no default).

741 passed. Each new browser test was mutation-checked against its fix;
the serialization test forces the race with a held first refresh, since
localhost alone never lost it.
This commit is contained in:
vh
2026-09-23 10:22:51 -07:00
parent fa5d46443d
commit 77833dc6d4
11 changed files with 622 additions and 82 deletions
+38 -5
View File
@@ -15,7 +15,10 @@ See docs/contracts/u1_item_record.contract.md.
from __future__ import annotations
import json
import os
import re
import stat
from dataclasses import dataclass
from pathlib import Path
from typing import Sequence
@@ -108,14 +111,44 @@ class Item:
SEEN_FILE = ".seen"
# A seen marker bigger than this is not one this service wrote: a JSON array of
# every rel in a 270-item booth is a few KB.
SEEN_MAX_BYTES = 1 << 20
def read_seen(booth: Path) -> set[str]:
"""Rels seen at full size. Missing or unreadable file -> empty set; a
damaged marker costs the tape its memory, never the page."""
"""Rels seen at full size (R2 C2). A JSON array of strings, because a rel
may hold a leading space or a newline and must round-trip exactly.
NEVER RAISES and NEVER BLOCKS. Any fleet session can write into a booth,
so the marker may be planted: it is opened without following a link and
without blocking (a FIFO with no writer), refused unless it is a regular
file of sane size, and anything unreadable or malformed reads as nothing
seen — a damaged marker costs the tape its memory, never the page.
"""
try:
text = (booth / SEEN_FILE).read_text()
except (OSError, UnicodeDecodeError):
fd = os.open(booth / SEEN_FILE, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)
except OSError:
return set()
return {ln.strip() for ln in text.splitlines() if ln.strip()}
try:
st = os.fstat(fd)
if not stat.S_ISREG(st.st_mode) or st.st_size > SEEN_MAX_BYTES:
return set()
raw = os.read(fd, SEEN_MAX_BYTES + 1)
except OSError:
return set()
finally:
os.close(fd)
try:
data = json.loads(raw.decode("utf-8"))
except (UnicodeDecodeError, ValueError, RecursionError):
# RecursionError: a deeply nested array (`[[[[...`) blows the parser's
# stack, and it is neither a ValueError nor an OSError — the same hole
# marks.py, manifest.py and benches.py already close.
return set()
if not isinstance(data, list):
return set()
return {r for r in data if isinstance(r, str)}
def read_blurred(booth: Path) -> set[str]: