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:
+38
-5
@@ -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]:
|
||||
|
||||
Reference in New Issue
Block a user