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:
co-authored by
Claude Fable 5
parent
f435cffea0
commit
e85f72c546
@@ -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 {
|
||||||
|
|||||||
@@ -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 || ""))
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}"'
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user