The operator found this in about a minute of using the live Desk: "images load
at full resolution instead of calculated thumbnails, which means they load VERY
slowly and are tiny."
MEASURED on the live set:
sindra-corpus-v1 66 images 77.5 MB 1024x1024 each
sindra-sfw-pool 59 images 71.7 MB
sindra 30 images 61.6 MB 2.1 MB average
sindra-bakeoff 40 images 57.2 MB
A tile renders around 250px, so the grid shipped roughly 16x the pixels that
reach the screen.
⚠ OUR PARKING RATIONALE WAS WRONG IN AN INSTRUCTIVE WAY. ROADMAP parked
progressive loading on "the largest gallery is 66 images; at that size a lazy
grid is almost certainly fine", and the parking-lot row said "270 <img
loading=lazy> may be fine". Both count IMAGES. Neither weighs BYTES. We measured
the dimension that was easy to measure rather than the one that determines the
experience, and 66 really is a fine count sitting on a terrible payload.
booth/thumbs.py caches WebP at 512px longest side inside the booth at
`.thumbs/<rel>.webp` — inside on purpose, so a cache can never outlive what it
describes. Pillow is an optional import: absent, every tile falls back to the
original, so the page is heavier and never broken. Generation is lazy, atomic
(temp + os.replace), rebuilt when the source is newer, and NEVER RAISES.
?thumb=1 rides the EXISTING file route rather than growing a new one, because
that route's traversal guard is already correct and a second route is a second
place to get it wrong.
ALSO FIXES A PRE-EXISTING LEAK THE CACHE WOULD HAVE WALKED INTO. booth_items and
zip_booth both tested `p.name.startswith(".")` — the FILE's name — so
`.thumbs/a.png` (name `a.png`) would have rendered as a gallery item and shipped
inside every zip. CLAUDE.md invariant 2 promises a dotfile costs nothing in item
counts, galleries or zips; that was true only at the top level. Both now skip
every dot-prefixed path COMPONENT.
AND THE FILMSTRIP, which is the same defect in a worse place: it shows EVERY
ring item at a few dozen pixels, so full-resolution frames there cost more than
the grid did. The stage is untouched and stays full size, because that is the
full-size review.
Item.thumb is derived in the resolver, not by a template reasoning about `kind`
(INV-1). build_gallery had to carry it too — a missing key there rendered as a
SILENT fallback to the full image, which is exactly where a new Item field gets
dropped with nothing failing.
754 green.
39 lines
1.5 KiB
TOML
39 lines
1.5 KiB
TOML
[project]
|
|
name = "booth"
|
|
version = "1.0.0b1"
|
|
description = "The Booth — a dead-simple standing web server that scans a data dir of drop-folders and renders each as an ephemeral media 'booth' (image/webm/audio auto-gallery, or a folder's own index.html verbatim). Also accepts browser/curl uploads for pickup under a human-readable id. 24h TTL, then the folder is wiped. Fleet tool for CC sessions to surface A/B and smoke results to the operator."
|
|
requires-python = ">=3.11"
|
|
dependencies = [
|
|
"fastapi>=0.115",
|
|
"uvicorn[standard]>=0.34",
|
|
"jinja2>=3.1",
|
|
"python-multipart>=0.0.9",
|
|
"markdown>=3.5",
|
|
"pillow>=10.0",
|
|
]
|
|
|
|
[project.optional-dependencies]
|
|
test = [
|
|
"pytest>=8.0",
|
|
"httpx>=0.27", # fastapi TestClient
|
|
# U3's embed seam moves placement into the browser, where no string
|
|
# assertion can see it. Browsers are NOT downloaded per project: they live
|
|
# box-wide in /opt/ms-playwright with PLAYWRIGHT_BROWSERS_PATH wired
|
|
# globally. THE UPPER BOUND IS THE POINT -- each playwright release pins a
|
|
# Chromium revision, and one that wants a revision the shared store does
|
|
# not have dies with an opaque "Executable doesn't exist" rather than a
|
|
# missing-dependency error. 1.60-1.62 map to chromium 1223/1228/1234, all
|
|
# present. Raise the bound only after the store has the newer revision.
|
|
"playwright>=1.60,<1.63",
|
|
]
|
|
|
|
[build-system]
|
|
requires = ["hatchling"]
|
|
build-backend = "hatchling.build"
|
|
|
|
[tool.hatch.build.targets.wheel]
|
|
packages = ["booth"]
|
|
|
|
[tool.pytest.ini_options]
|
|
testpaths = ["tests"]
|