diff --git a/booth/app.py b/booth/app.py index 918e07d..b29fc6b 100644 --- a/booth/app.py +++ b/booth/app.py @@ -932,6 +932,14 @@ def create_app( # every other booth, so this pair is read only when it renders. # `read_benches` never raises; a damaged registry costs its own # panel and says so, which is the v0.2.2 lesson. + # `is_board` is PAGE IDENTITY, not page content. Gating the + # panel on `board or benches` hid it — and its registration + # form — exactly when the board was empty and the registry + # absent, which is the state a new deployment starts in and the + # one where "no benches registered yet" is most worth saying. + # A panel that disappears when it has nothing to show is the + # same defect as a damaged panel rendering as an absent one. + "is_board": (booth / LINKS_FILE).is_file(), **dict(zip(("benches", "benches_error"), read_benches(data_dir) if (booth / LINKS_FILE).is_file() else ([], None))), diff --git a/booth/benches.py b/booth/benches.py index a06a34f..bb8fc8e 100644 --- a/booth/benches.py +++ b/booth/benches.py @@ -136,6 +136,14 @@ def normalize_bench_url(url: str) -> str: if not host: raise ValueError("that URL has no host") + # RE-WRAP A BRACKETED IPv6 LITERAL. `urlsplit().hostname` strips the + # brackets, and rebuilding the netloc from it produces `http://::1:8080/a` + # — not a different spelling of the same URL but a BROKEN one, so a re-post + # never matches the row the operator thinks they are updating. The bracket + # is part of the authority's syntax, not decoration. Detected by the colon, + # which cannot appear in a hostname or an IPv4 literal. + if ":" in host: + host = f"[{host}]" default = {"http": 80, "https": 443}[scheme] netloc = host if port in (None, default) else f"{host}:{port}" # A bare "/" is the same resource as no path at all; a trailing slash on a @@ -240,6 +248,12 @@ def read_benches(root: Path) -> tuple[list[Bench], str | None]: return [], str(exc) except OSError as exc: return [], f"registry could not be read: {exc}" + except RecursionError: + # Deeply nested JSON (`[[[[...`) blows the stack inside json.loads, and + # RecursionError is neither ValueError nor OSError — so it escaped the + # pair above and 500'd the page this function exists to protect. The + # byte cap does not help: 200k open brackets is 200 KB. + return [], "registry is nested too deeply to parse" def _write_all(root: Path, benches: dict[str, Bench]) -> None: diff --git a/booth/templates/base.html b/booth/templates/base.html index 737544d..6a1c448 100644 --- a/booth/templates/base.html +++ b/booth/templates/base.html @@ -511,6 +511,7 @@ .bench-row.is-promoted .bench-state{background:rgba(130,170,240,.18)} .bench-main{flex:1;min-width:0} .bench-url{font-size:.78em;opacity:.55;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} + .bench-meta{display:flex;flex-direction:column;align-items:flex-end;font-size:.75em;opacity:.6} .bench-acts{display:flex;gap:.3rem} .bench-to,.bench-rm{font-size:.75em;padding:.15rem .4rem;cursor:pointer} .bench-add{display:flex;gap:.4rem;padding:.5rem .7rem;border-top:1px solid var(--line,#2a2a2a)} diff --git a/booth/templates/booth.html b/booth/templates/booth.html index 0060efc..f4f76f0 100644 --- a/booth/templates/booth.html +++ b/booth/templates/booth.html @@ -66,7 +66,7 @@ {% else %}