Compare commits

...
2 Commits
Author SHA1 Message Date
hermes 81ebdf3c76 fix(ui): stop the pages being cached, and the labels duplicating (#19)
Build and Push Image / Build and push image (push) Successful in 3m29s
fix(ui): stop the pages being cached, and the labels duplicating (#19)
2026-11-09 22:06:58 +10:00
hermes e473850aac fix(ui): stop the pages being cached, and the labels duplicating
Firefox served a cached copy of the brewing page after the advice
became a button and a pop-out, so the phone kept showing the old
inline advice - a long paragraph in the table cell, with the button
label reading "Read the adviceAdvice".

Two separate causes:

The pages had no cache headers, so Firefox was free to reuse them.
They now send Cache-Control: no-store, must-revalidate.

The rule that hides the short label was scoped to `table.data .short`,
so it did not apply to the advice button's label.  A stale stylesheet
then showed both forms at once.  The rule is now on `.short` alone, so
it applies wherever the pattern is used - a table heading, the advice
button, the Active Beers page - and only ever one form can show.

Verified at 390px and 1280px: the phone shows "Advice", the desktop
shows "Read the advice" and "BREWING ADVICE", and no element ever
shows both.

192 tests pass.
2026-11-09 22:06:42 +10:00
4 changed files with 70 additions and 21 deletions
+12
View File
@@ -87,6 +87,18 @@ def create_app(config: dict | None = None) -> Flask:
if config:
app.config.update(config)
# The pages must not be cached by the browser.
# A phone held on to a cached copy of the brewing page and showed
# the old inline advice instead of the button and the pop-out, so
# the change was not visible until the cache was cleared by hand.
@app.after_request
def _no_store(response):
if response.mimetype == "text/html":
response.headers["Cache-Control"] = "no-store, must-revalidate"
response.headers["Pragma"] = "no-cache"
return response
csrf = CSRFProtect(app)
# ------------------------------------------------------------------ #
+16 -21
View File
@@ -484,9 +484,14 @@ table.data .num { font-family: var(--mono); text-align: right; white-space: nowr
table.data .comment { color: var(--text-muted); font-size: .84rem; }
/* A heading that is too long for a phone column. The long form shows
on a desktop and the short form on a phone - see the media query. */
table.data .short { display: none; }
table.data .advice-trigger .short { display: none; }
on a desktop and the short form on a phone - see the media query.
The rule is on `.short` alone, not on `table.data .short`, so it
applies everywhere the pattern is used - in a table heading, in the
advice button, and on the Active Beers page. When only the table
was covered, the advice button could show both forms at once and
read "Read the adviceAdvice". */
.short { display: none; }
/* On a narrow screen the header wraps onto two rows: the brand and the
sign-out on the first, the nav tabs on the second where they get the
@@ -570,19 +575,17 @@ table.data .advice-trigger .short { display: none; }
overflow-wrap: break-word;
}
/* The heading gets a short form, because a heading that is cut off
is worse than a heading that is brief. */
table.data .col-abv .long,
table.data .col-advice .long,
table.data .col-finished .long,
table.data .col-comment .long {
/* The long form gives way to the short form on a phone, because a
heading that is cut off is worse than a heading that is brief.
The rule is on the span itself rather than on the column, so it
applies everywhere the pattern is used - in a table heading, in
the advice button, and on the Active Beers page. */
.long {
display: none;
}
table.data .col-abv .short,
table.data .col-advice .short,
table.data .col-finished .short,
table.data .col-comment .short {
.short {
display: inline;
}
@@ -594,14 +597,6 @@ table.data .advice-trigger .short { display: none; }
gap: .2rem;
}
table.data .advice-trigger .long {
display: none;
}
table.data .advice-trigger .short {
display: inline;
}
/* The action column holds a button. */
table.data .col-action {
white-space: nowrap;
+12
View File
@@ -714,3 +714,15 @@ def test_the_api_exposes_the_attenuation_and_curve(logged_in, repo):
assert len(entry["curve"]) == 2
assert entry["curve"][0]["progress"] == 0.0
assert entry["curve"][1]["progress"] == 1.0
def test_the_pages_are_not_cached(client):
"""The HTML pages must not be cached by the browser.
A phone held on to a cached copy of the brewing page and kept
showing the old inline advice after the button and the pop-out had
been deployed, so the change was invisible until the cache was
cleared by hand.
"""
response = client.get("/login")
assert "no-store" in response.headers.get("Cache-Control", "")
+30
View File
@@ -1049,6 +1049,36 @@ def test_the_tables_keep_their_columns_on_a_phone(client, repo):
assert "Cider raspberry elderberry" in body
def test_the_long_and_short_labels_are_both_in_the_page(client, repo):
"""The long and the short label are both in the markup.
CSS picks which one shows - the long form on a desktop, the short
form on a phone. If only one is in the markup the other cannot be
shown at all, and if the CSS rule misses one of them the page shows
"Read the adviceAdvice".
This is a markup assertion, not a CSS assertion: it makes sure
both forms exist for the stylesheet to choose between.
"""
repo.users.insert_one({"username": "beer", "password": "beer_pass"})
client.post("/login", data={"username": "beer", "password": "beer_pass"},
follow_redirects=True)
seed_readings(repo, [("1", "Ale", "1042", "2026-01-01", False, "")])
doc = repo.readings.find_one({"beer_run_id": "1"})
repo.set_reading_advice(str(doc["_id"]), "Warm it to 21C and wait.", ok=True)
body = client.get("/brews").get_data(as_text=True)
# The heading has both forms.
assert '<span class="long">Brewing advice</span>' in body
assert '<span class="short">Advice</span>' in body
# The button has both forms.
assert '<span class="long">Read the advice</span>' in body
assert '<span class="short">Advice</span>' in body
def test_a_failed_advice_is_marked_as_such(client, repo):
"""A generation failure reads differently from real advice."""
repo.users.insert_one({"username": "beer", "password": "beer_pass"})