The where-it-lives card actually behaves; assets stop serving stale

Eric, again with the screenshot that tells the truth: "when I click it
it greys out and nothing seems to happen." Two roots. My own
#storedform { display:flex } silently DEFEATED the hidden attribute
(an author display beats the UA's [hidden] rule), so the form was
visible from load and the click's only visible effect was disabling
the link. The rule is now :not([hidden])-scoped, the click swaps the
sentence for the form and focuses the select. And the "unstyled"
select he saw was last week's stylesheet: /static assets had no
cache-busting, so my fresh-fetch Chrome verification passed while his
browser held the old CSS. Asset URLs now carry ?v=<app version> —
an upgrade busts every browser's cache by construction.

Verified the full interaction in-browser this time: form hidden on
load, click reveals with 142 container options in a styled select.

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-09 13:14:55 -04:00
co-authored by Claude Fable 5
parent f435cffea0
commit e85f72c546
5 changed files with 29 additions and 8 deletions
+4 -1
View File
@@ -536,7 +536,10 @@ select {
border: 2px solid var(--board-edge); border-radius: var(--radius); border: 2px solid var(--board-edge); border-radius: var(--radius);
padding: .25rem .45rem; background: #fff; max-width: 24rem; padding: .25rem .45rem; background: #fff; max-width: 24rem;
} }
#storedform { display: flex; gap: .5rem; align-items: center; } /* :not([hidden]) so the hidden attribute still works — a bare
display:flex would override the browser's [hidden] rule and show
the form permanently */
#storedform:not([hidden]) { display: flex; gap: .5rem; align-items: center; }
#storedform select { flex: 1 1 auto; } #storedform select { flex: 1 1 auto; }
.empty { .empty {
+3 -2
View File
@@ -74,11 +74,12 @@ function wireStored(g) {
const btn = document.getElementById("editstored"); const btn = document.getElementById("editstored");
if (!btn) return; if (!btn) return;
btn.addEventListener("click", async () => { btn.addEventListener("click", async () => {
btn.closest("p").hidden = true; // the sentence yields to the form
const form = document.getElementById("storedform"); const form = document.getElementById("storedform");
form.hidden = false; form.hidden = false;
btn.disabled = true;
const all = await fetchJSON("/api/library");
const pick = document.getElementById("storedpick"); const pick = document.getElementById("storedpick");
pick.focus();
const all = await fetchJSON("/api/library");
pick.innerHTML = `<option value="">its own box</option>` + all pick.innerHTML = `<option value="">its own box</option>` + all
.filter(o => o.bgg_id && o.bgg_id !== g.bgg_id) .filter(o => o.bgg_id && o.bgg_id !== g.bgg_id)
.sort((a, b) => (a.name || "").localeCompare(b.name || "")) .sort((a, b) => (a.name || "").localeCompare(b.name || ""))
+2 -2
View File
@@ -7,7 +7,7 @@
<link rel="icon" type="image/png" href="/static/favicon.png"> <link rel="icon" type="image/png" href="/static/favicon.png">
<link rel="apple-touch-icon" href="/static/apple-touch-icon.png"> <link rel="apple-touch-icon" href="/static/apple-touch-icon.png">
<meta name="apple-mobile-web-app-title" content="bggpipe"> <meta name="apple-mobile-web-app-title" content="bggpipe">
<link rel="stylesheet" href="/static/app.css"> <link rel="stylesheet" href="/static/app.css?v=__ASSET_V__">
</head> </head>
<body> <body>
<a class="skip" href="#main">Skip to content</a> <a class="skip" href="#main">Skip to content</a>
@@ -32,7 +32,7 @@
</div> </div>
<div class="content"> <div class="content">
<div id="banner" role="status"></div> <div id="banner" role="status"></div>
<script src="/static/app.js"></script> <script src="/static/app.js?v=__ASSET_V__"></script>
<main id="main"> <main id="main">
<!--PAGE--> <!--PAGE-->
</main> </main>
+7 -1
View File
@@ -724,7 +724,13 @@ def create_app(
(a detail page keeps its section lit).""" (a detail page keeps its section lit)."""
active = active or name active = active or name
templates = resources.files("bggpipe") / "templates" templates = resources.files("bggpipe") / "templates"
shell = (templates / "shell.html").read_text() # version-stamped asset URLs: a browser that cached last week's
# stylesheet must fetch the new one the moment the app upgrades
from bggpipe import __version__
shell = (
(templates / "shell.html").read_text().replace("__ASSET_V__", __version__)
)
fragment = (templates / "pages" / f"{name}.html").read_text() fragment = (templates / "pages" / f"{name}.html").read_text()
nav = "\n".join( nav = "\n".join(
f' <a href="{href}"' f' <a href="{href}"'
+13 -2
View File
@@ -205,7 +205,7 @@ def test_stylesheet_is_served_and_linked_by_every_page(tmp_path):
assert css.headers["content-type"].startswith("text/css") assert css.headers["content-type"].startswith("text/css")
assert "--accent" in css.text # the token layer, not an empty file assert "--accent" in css.text # the token layer, not an empty file
for path in ("/", "/review"): for path in ("/", "/review"):
assert 'href="/static/app.css"' in web.get(path).text assert 'href="/static/app.css?v=' in web.get(path).text
def test_every_page_marks_itself_current_in_the_nav(tmp_path): def test_every_page_marks_itself_current_in_the_nav(tmp_path):
@@ -653,7 +653,7 @@ def test_photo_detail_page_serves_with_photos_nav_active(tmp_path):
(current,) = re.findall(r'<a href="([^"]+)" aria-current="page"', html) (current,) = re.findall(r'<a href="([^"]+)" aria-current="page"', html)
assert current == "/photos" assert current == "/photos"
assert 'src="/static/app.js"' in html assert 'src="/static/app.js?v=' in html
def test_pipeline_counts_pending_work_not_queue_rows(tmp_path): def test_pipeline_counts_pending_work_not_queue_rows(tmp_path):
@@ -675,3 +675,14 @@ def test_pipeline_counts_pending_work_not_queue_rows(tmp_path):
p = _app(cfg).get("/api/pipeline").json() p = _app(cfg).get("/api/pipeline").json()
assert p["to_update"] == 0 # applied assert p["to_update"] == 0 # applied
assert p["queued_total"] == 1 # still listed until diff reruns assert p["queued_total"] == 1 # still listed until diff reruns
def test_assets_are_version_stamped(tmp_path):
"""A cached stylesheet from last week must not survive an upgrade:
asset URLs carry the app version."""
from bggpipe import __version__
html = _app(_cfg(tmp_path)).get("/").text
assert f"/static/app.css?v={__version__}" in html
assert f"/static/app.js?v={__version__}" in html
assert "__ASSET_V__" not in html