diff --git a/src/flask/app_factory.py b/src/flask/app_factory.py
index 5655d8e..348dbd6 100644
--- a/src/flask/app_factory.py
+++ b/src/flask/app_factory.py
@@ -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)
# ------------------------------------------------------------------ #
diff --git a/src/flask/static/css/app.css b/src/flask/static/css/app.css
index b419c99..11dd675 100644
--- a/src/flask/static/css/app.css
+++ b/src/flask/static/css/app.css
@@ -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;
diff --git a/tests/test_app.py b/tests/test_app.py
index 091f020..7ec8095 100644
--- a/tests/test_app.py
+++ b/tests/test_app.py
@@ -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", "")
diff --git a/tests/test_brew_advice.py b/tests/test_brew_advice.py
index a9c438b..6144911 100644
--- a/tests/test_brew_advice.py
+++ b/tests/test_brew_advice.py
@@ -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 'Brewing advice' in body
+ assert 'Advice' in body
+
+ # The button has both forms.
+ assert 'Read the advice' in body
+ assert 'Advice' 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"})