feat(prezentacja): cache-busting plików statycznych (PRE-26)
Testy / Testy warstwy logicznej (silnik) (pull_request) Successful in 10m29s
Testy / Testy warstwy prezentacji (dostęp do baz) (pull_request) Successful in 9m36s
Testy / Build obrazu silnika B (swisseph) (pull_request) Successful in 11s
Testy / Kontrola składni wszystkich warstw (pull_request) Successful in 8s
build / build (push) Successful in 50s
Testy / Testy warstwy logicznej (silnik) (push) Successful in 10m51s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m29s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 11s
Testy / Kontrola składni wszystkich warstw (push) Successful in 8s
Testy / Testy warstwy logicznej (silnik) (pull_request) Successful in 10m29s
Testy / Testy warstwy prezentacji (dostęp do baz) (pull_request) Successful in 9m36s
Testy / Build obrazu silnika B (swisseph) (pull_request) Successful in 11s
Testy / Kontrola składni wszystkich warstw (pull_request) Successful in 8s
build / build (push) Successful in 50s
Testy / Testy warstwy logicznej (silnik) (push) Successful in 10m51s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m29s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 11s
Testy / Kontrola składni wszystkich warstw (push) Successful in 8s
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=<md5[:8]>`. 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 <noreply@anthropic.com>
This commit was merged in pull request #48.
This commit is contained in:
@@ -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
|
# build-marker: 2026-07-25 wymuszenie nowego obrazu po incydencie z tagiem :latest
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
import json
|
import json
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
|
from functools import lru_cache
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from fastapi import FastAPI, Form, HTTPException, Query, Request
|
from fastapi import FastAPI, Form, HTTPException, Query, Request
|
||||||
@@ -30,6 +33,29 @@ logic = LogicClient()
|
|||||||
security.install(app) # logowanie + limit żądań (LOG-32)
|
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]:
|
def _build_utc(date: str, time: str, tz_offset: float) -> tuple[str, str]:
|
||||||
"""Z lokalnej daty/godziny + przesunięcia strefy → moment UTC.
|
"""Z lokalnej daty/godziny + przesunięcia strefy → moment UTC.
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{# Wyszukiwarka lokalizacji + interaktywna mapa (OpenStreetMap / Leaflet, bez klucza API).
|
{# Wyszukiwarka lokalizacji + interaktywna mapa (OpenStreetMap / Leaflet, bez klucza API).
|
||||||
Wklejana do formularzy z polami input[name=lat] / input[name=lon]. Samowystarczalna:
|
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. #}
|
ładuje CSS/JS Leafleta (vendorowane lokalnie) oraz geo.js. Kafelki mapy lecą z OSM. #}
|
||||||
<link rel="stylesheet" href="/static/vendor/leaflet/leaflet.css">
|
<link rel="stylesheet" href="{{ static('vendor/leaflet/leaflet.css') }}">
|
||||||
<div class="geo" id="geoPicker">
|
<div class="geo" id="geoPicker">
|
||||||
<div class="geo-search">
|
<div class="geo-search">
|
||||||
<input type="search" id="geoSearch" autocomplete="off"
|
<input type="search" id="geoSearch" autocomplete="off"
|
||||||
@@ -16,5 +16,5 @@
|
|||||||
contributors (Nominatim). Kliknij mapę lub przeciągnij pineskę, by ustawić współrzędne.
|
contributors (Nominatim). Kliknij mapę lub przeciągnij pineskę, by ustawić współrzędne.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<script defer src="/static/vendor/leaflet/leaflet.js"></script>
|
<script defer src="{{ static('vendor/leaflet/leaflet.js') }}"></script>
|
||||||
<script defer src="/static/geo.js"></script>
|
<script defer src="{{ static('geo.js') }}"></script>
|
||||||
|
|||||||
@@ -4,15 +4,15 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>astrololo · {% block title %}{% endblock %}</title>
|
<title>astrololo · {% block title %}{% endblock %}</title>
|
||||||
<link rel="stylesheet" href="/static/styles.css">
|
<link rel="stylesheet" href="{{ static('styles.css') }}">
|
||||||
{# Wspólne dane formularza między zakładkami (PRE-21). W <head> z `defer`
|
{# Wspólne dane formularza między zakładkami (PRE-21). W <head> z `defer`
|
||||||
CELOWO: skrypty defer wykonują się w kolejności dokumentu, więc ten zdąży
|
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
|
odtworzyć współrzędne, ZANIM geo.js zbuduje mapę — mapa startuje od razu
|
||||||
we właściwym miejscu, bez przestawiania. #}
|
we właściwym miejscu, bez przestawiania. #}
|
||||||
<script defer src="/static/formsync.js"></script>
|
<script defer src="{{ static('formsync.js') }}"></script>
|
||||||
{# Powiększanie kosmogramu (PRE-25) — globalnie, bo koło pojawi się też na
|
{# Powiększanie kosmogramu (PRE-25) — globalnie, bo koło pojawi się też na
|
||||||
zakładce „Skompiluj"; skrypt sam sprawdza, czy jest co powiększać. #}
|
zakładce „Skompiluj"; skrypt sam sprawdza, czy jest co powiększać. #}
|
||||||
<script defer src="/static/wheelzoom.js"></script>
|
<script defer src="{{ static('wheelzoom.js') }}"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main>
|
<main>
|
||||||
|
|||||||
@@ -340,5 +340,5 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<script src="/static/now.js"></script>
|
<script src="{{ static('now.js') }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -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 —
|
{# 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). #}
|
każdy sprawdza, czy jego kontener jest na stronie (tu go nie ma). #}
|
||||||
<script src="/static/natal.js"></script>
|
<script src="{{ static('natal.js') }}"></script>
|
||||||
<script src="/static/predictions.js"></script>
|
<script src="{{ static('predictions.js') }}"></script>
|
||||||
<script src="/static/compile.js"></script>
|
<script src="{{ static('compile.js') }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -97,10 +97,10 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<script src="/static/now.js"></script>
|
<script src="{{ static('now.js') }}"></script>
|
||||||
<script src="/static/copy.js"></script>
|
<script src="{{ static('copy.js') }}"></script>
|
||||||
<script src="/static/models.js"></script>
|
<script src="{{ static('models.js') }}"></script>
|
||||||
<script src="/static/progress.js"></script>
|
<script src="{{ static('progress.js') }}"></script>
|
||||||
{# po progress.js — nasłuchuje zdarzenia o gotowej interpretacji (PRE-23) #}
|
{# po progress.js — nasłuchuje zdarzenia o gotowej interpretacji (PRE-23) #}
|
||||||
<script src="/static/natal.js"></script>
|
<script src="{{ static('natal.js') }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -91,10 +91,10 @@
|
|||||||
<p class="muted small" id="predictionsNote"></p>
|
<p class="muted small" id="predictionsNote"></p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<script src="/static/now.js"></script>
|
<script src="{{ static('now.js') }}"></script>
|
||||||
<script src="/static/copy.js"></script>
|
<script src="{{ static('copy.js') }}"></script>
|
||||||
<script src="/static/models.js"></script>
|
<script src="{{ static('models.js') }}"></script>
|
||||||
<script src="/static/progress.js"></script>
|
<script src="{{ static('progress.js') }}"></script>
|
||||||
{# po progress.js — nasłuchuje zdarzenia, które tamten wysyła po gotowym horoskopie #}
|
{# po progress.js — nasłuchuje zdarzenia, które tamten wysyła po gotowym horoskopie #}
|
||||||
<script src="/static/predictions.js"></script>
|
<script src="{{ static('predictions.js') }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -106,9 +106,10 @@ def test_stores_are_loaded_on_the_tab():
|
|||||||
|
|
||||||
|
|
||||||
def test_stores_load_before_the_assembler():
|
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):
|
def tag(name):
|
||||||
return TPL.index('src="/static/' + name + '"')
|
return TPL.index("static('" + name + "')")
|
||||||
|
|
||||||
assert tag("natal.js") < tag("compile.js")
|
assert tag("natal.js") < tag("compile.js")
|
||||||
assert tag("predictions.js") < tag("compile.js")
|
assert tag("predictions.js") < tag("compile.js")
|
||||||
|
|||||||
Reference in New Issue
Block a user