From a0d1135db1b9c4e84bbe606c7bf593a704d80bd1 Mon Sep 17 00:00:00 2001 From: migatu Date: Mon, 3 Aug 2026 11:22:38 +0200 Subject: [PATCH] =?UTF-8?q?feat(prezentacja):=20cache-busting=20plik=C3=B3?= =?UTF-8?q?w=20statycznych=20(PRE-26)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Po deployu przeglądarka trzymała stare styles.css / *.js — ten sam URL, więc serwowała z cache mimo nowej wersji. Doklejamy do URL-a krótki HASH TREŚCI pliku: zmienił się plik → zmienił się URL → przeglądarka pobiera nowy; bez zmian URL zostaje ten sam i cache dalej działa (bustujemy tylko to, co się zmieniło). - `static_url(name)` + globalny helper Jinja `static()`: `/static/x?v=`. Hash liczony raz na proces (lru_cache) — nowy pod po deployu = świeży hash; brak pliku → `?v=0`, nie wywala strony. - Wszystkie odwołania w szablonach (styles.css, nasze *.js, vendor Leaflet) idą teraz przez helper zamiast surowego `/static/...`. Testy: +5 (hash w URL, zależny od treści, brak-pliku-bezpieczny, żaden szablon nie serwuje surowej ścieżki, base używa helpera). Test kolejności skryptów zaktualizowany pod nowy format. Prezentacja 214. Co-Authored-By: Claude Opus 4.8 --- services/presentation/app/main.py | 26 ++++++++++++ .../app/templates/_location_picker.html | 6 +-- services/presentation/app/templates/base.html | 6 +-- .../presentation/app/templates/chart.html | 2 +- .../presentation/app/templates/compile.html | 6 +-- .../presentation/app/templates/interpret.html | 10 ++--- .../presentation/app/templates/timeline.html | 10 ++--- .../presentation/tests/test_cache_busting.py | 41 +++++++++++++++++++ services/presentation/tests/test_compile.py | 5 ++- 9 files changed, 90 insertions(+), 22 deletions(-) create mode 100644 services/presentation/tests/test_cache_busting.py diff --git a/services/presentation/app/main.py b/services/presentation/app/main.py index bce50fc..64807e0 100644 --- a/services/presentation/app/main.py +++ b/services/presentation/app/main.py @@ -10,8 +10,11 @@ Strona główna „/" = wprowadzenie danych horoskopu i podgląd policzonych poz # build-marker: 2026-07-25 wymuszenie nowego obrazu po incydencie z tagiem :latest from __future__ import annotations +import hashlib import json from datetime import datetime, timedelta, timezone +from functools import lru_cache +from pathlib import Path import httpx from fastapi import FastAPI, Form, HTTPException, Query, Request @@ -30,6 +33,29 @@ logic = LogicClient() security.install(app) # logowanie + limit żądań (LOG-32) +# ── cache-busting statyki (PRE-26) ───────────────────────────────────────── +# Po deployu przeglądarka trzymała stare styles.css / *.js (ten sam URL → cache). +# Doklejamy do URL-a krótki HASH TREŚCI pliku: zmieni się plik → zmieni się URL → +# przeglądarka pobierze nowy; bez zmian URL zostaje ten sam (cache działa dalej). +# Hash liczony raz na proces (lru_cache) — nowy pod po deployu = świeży hash. +_STATIC_DIR = Path("app/static") + + +@lru_cache(maxsize=None) +def _asset_version(name: str) -> str: + try: + return hashlib.md5((_STATIC_DIR / name).read_bytes()).hexdigest()[:8] + except OSError: + return "0" # brak pliku nie może wywrócić strony + + +def static_url(name: str) -> str: + return f"/static/{name}?v={_asset_version(name)}" + + +templates.env.globals["static"] = static_url + + def _build_utc(date: str, time: str, tz_offset: float) -> tuple[str, str]: """Z lokalnej daty/godziny + przesunięcia strefy → moment UTC. diff --git a/services/presentation/app/templates/_location_picker.html b/services/presentation/app/templates/_location_picker.html index 8edc006..e81b77d 100644 --- a/services/presentation/app/templates/_location_picker.html +++ b/services/presentation/app/templates/_location_picker.html @@ -1,7 +1,7 @@ {# Wyszukiwarka lokalizacji + interaktywna mapa (OpenStreetMap / Leaflet, bez klucza API). Wklejana do formularzy z polami input[name=lat] / input[name=lon]. Samowystarczalna: ładuje CSS/JS Leafleta (vendorowane lokalnie) oraz geo.js. Kafelki mapy lecą z OSM. #} - +
- - + + diff --git a/services/presentation/app/templates/base.html b/services/presentation/app/templates/base.html index 71ec1e2..43c99c9 100644 --- a/services/presentation/app/templates/base.html +++ b/services/presentation/app/templates/base.html @@ -4,15 +4,15 @@ astrololo · {% block title %}{% endblock %} - + {# Wspólne dane formularza między zakładkami (PRE-21). W z `defer` CELOWO: skrypty defer wykonują się w kolejności dokumentu, więc ten zdąży odtworzyć współrzędne, ZANIM geo.js zbuduje mapę — mapa startuje od razu we właściwym miejscu, bez przestawiania. #} - + {# Powiększanie kosmogramu (PRE-25) — globalnie, bo koło pojawi się też na zakładce „Skompiluj"; skrypt sam sprawdza, czy jest co powiększać. #} - +
diff --git a/services/presentation/app/templates/chart.html b/services/presentation/app/templates/chart.html index 6ef6549..c97f738 100644 --- a/services/presentation/app/templates/chart.html +++ b/services/presentation/app/templates/chart.html @@ -340,5 +340,5 @@ {% endif %} {% endif %} - + {% endblock %} diff --git a/services/presentation/app/templates/compile.html b/services/presentation/app/templates/compile.html index a8dfb57..aade5de 100644 --- a/services/presentation/app/templates/compile.html +++ b/services/presentation/app/templates/compile.html @@ -135,7 +135,7 @@ zapamiętane predykcje okresowe. Dane pobiera z pozostałych zakładek — nie t {# Magazyny wczytujemy dla ich API odczytu. Ich własne UI samo się wyłącza — każdy sprawdza, czy jego kontener jest na stronie (tu go nie ma). #} - - - + + + {% endblock %} diff --git a/services/presentation/app/templates/interpret.html b/services/presentation/app/templates/interpret.html index f3772ca..29c7691 100644 --- a/services/presentation/app/templates/interpret.html +++ b/services/presentation/app/templates/interpret.html @@ -97,10 +97,10 @@ {% endfor %} {% endif %} - - - - + + + + {# po progress.js — nasłuchuje zdarzenia o gotowej interpretacji (PRE-23) #} - + {% endblock %} diff --git a/services/presentation/app/templates/timeline.html b/services/presentation/app/templates/timeline.html index 8c0fc59..90cf9e2 100644 --- a/services/presentation/app/templates/timeline.html +++ b/services/presentation/app/templates/timeline.html @@ -91,10 +91,10 @@

- - - - + + + + {# po progress.js — nasłuchuje zdarzenia, które tamten wysyła po gotowym horoskopie #} - + {% endblock %} diff --git a/services/presentation/tests/test_cache_busting.py b/services/presentation/tests/test_cache_busting.py new file mode 100644 index 0000000..56d37a6 --- /dev/null +++ b/services/presentation/tests/test_cache_busting.py @@ -0,0 +1,41 @@ +"""Cache-busting plików statycznych (PRE-26). + +Po deployu przeglądarka trzymała stare styles.css / *.js (ten sam URL). Doklejamy +hash treści → zmiana pliku zmienia URL. Regresja byłaby CICHA (użytkownik widzi +stary interfejs), więc pilnujemy strukturalnie. +""" +import os +import pathlib +import re + +os.environ.pop("APP_PASSWORD", None) # import app.main bez bramki logowania + +from app.main import _asset_version, static_url + +TEMPLATES = pathlib.Path(__file__).resolve().parents[1] / "app" / "templates" + + +def test_static_url_carries_content_hash(): + assert re.fullmatch(r"/static/styles\.css\?v=[0-9a-f]{8}", static_url("styles.css")) + + +def test_version_is_content_based_so_differs_between_files(): + assert _asset_version("styles.css") != _asset_version("formsync.js") + + +def test_missing_file_does_not_crash(): + assert static_url("nie-ma-takiego.js").endswith("?v=0") + + +def test_no_template_serves_raw_static_paths(): + """Żaden href/src nie może iść na surowe /static/... — omijałoby cache-busting.""" + for f in TEMPLATES.glob("*.html"): + txt = f.read_text(encoding="utf-8") + assert 'src="/static/' not in txt, f"{f.name}: surowy src bez cache-bustingu" + assert 'href="/static/' not in txt, f"{f.name}: surowy href bez cache-bustingu" + + +def test_base_uses_helper_for_stylesheet_and_scripts(): + base = (TEMPLATES / "base.html").read_text(encoding="utf-8") + assert "static('styles.css')" in base + assert "static('formsync.js')" in base diff --git a/services/presentation/tests/test_compile.py b/services/presentation/tests/test_compile.py index cfb9e50..1f73ec9 100644 --- a/services/presentation/tests/test_compile.py +++ b/services/presentation/tests/test_compile.py @@ -106,9 +106,10 @@ def test_stores_are_loaded_on_the_tab(): def test_stores_load_before_the_assembler(): - """Szukamy TAGÓW skryptów, nie samych nazw — te padają też w komentarzach.""" + """Szukamy TAGÓW skryptów, nie samych nazw — te padają też w komentarzach. + Statyka jedzie przez helper cache-bustingu (PRE-26): src="{{ static('X') }}".""" def tag(name): - return TPL.index('src="/static/' + name + '"') + return TPL.index("static('" + name + "')") assert tag("natal.js") < tag("compile.js") assert tag("predictions.js") < tag("compile.js")