Mobile: every page earns its phone layout, and --lan serves the network

Verified page by page in a 390px viewport against live data. The rail
collapses to a brand line plus one horizontally scrollable nav row
(was: two wrapped rows). The Titles table becomes stacked line-cards —
title, chips, match, photos, finger-sized actions — with empty cells
dropped (the templates now emit them tight so :empty applies); the
editor stacks full-width inputs. Queue/library ledgers scroll inside
their card and photo lists wrap at commas instead of clipping. The
review done-card stats wrap, keyboard hints hide on touch widths,
reshoot stencils go horizontal, buttons get touch-sized padding.
Also: post-rename copy on the remove button.

bggpipe web --lan binds 0.0.0.0 and extends the mutation guard's host
allowlist with this machine's names and addresses (never a wildcard —
DNS-rebinding arrives under the attacker's hostname, which an allowlist
rejects), prints every reachable URL, and warns loudly that the app has
no login. Default stays localhost-only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
This commit is contained in:
Eric Wagoner
2026-08-03 16:02:08 -04:00
co-authored by Claude Fable 5
parent 429c519237
commit 2936d21f0a
8 changed files with 179 additions and 27 deletions
+31
View File
@@ -869,3 +869,34 @@ def test_confirm_marks_shaky_read_verified_without_requeue(tmp_path):
).status_code
== 400
)
def test_lan_allowed_hosts_admit_network_but_not_strangers(tmp_path):
cfg = make_cfg(tmp_path)
app = create_app(
cfg,
client=unauthorized_client(tmp_path),
allowed_hosts={"192.168.1.5", "erics-mac.local"},
)
body = {"title_raw": "Citadels", "source_photos": "shelf.jpg", "confirm": True}
lan = TestClient(app, base_url="http://192.168.1.5")
assert lan.post("/api/edit-title", json=body).status_code == 200
# a hostname NOT on the allowlist (DNS rebinding shape) is still refused
stranger = TestClient(app, base_url="http://attacker.example")
assert stranger.post("/api/edit-title", json=body).status_code == 403
# and without the opt-in, the LAN host is refused too
plain = create_app(cfg, client=unauthorized_client(tmp_path))
assert (
TestClient(plain, base_url="http://192.168.1.5")
.post("/api/edit-title", json=body)
.status_code
== 403
)
def test_lan_hosts_reports_this_machine(tmp_path):
from bggpipe.webreview import lan_hosts
hosts = lan_hosts()
assert hosts # at least the hostname
assert all(h == h.lower() or "." in h for h in hosts)