mobile: cache-bust static assets so browser picks up CSS/JS edits

User reported phone still showing old behaviour (Qty/Avg portfolio
columns visible) even though the server-side JS had been updated.
Root cause: every <link>/<script> URL was a plain
/static/css/foo.css with no query string, so mobile Chrome served
the file from its HTTP cache rather than refetching it.

Adds a process-startup timestamp to the Jinja environment as
ASSET_VERSION (computed once when templates_env is imported). Every
<link>/<script> reference now appends `?v={{ ASSET_VERSION }}` so a
container restart bumps the URL and the browser refetches. 38 URLs
across 8 templates updated via sed; tests still pass.

Side benefit: future CSS/JS edits no longer require users to hard-
refresh.
This commit is contained in:
Giorgio Gilestro 2026-05-28 19:20:49 +02:00
parent 1a20f0a15b
commit daa3f79a52
9 changed files with 47 additions and 38 deletions

View file

@ -36,16 +36,16 @@
} catch (e) { document.documentElement.dataset.theme = 'light'; }
})();
</script>
<link rel="stylesheet" href="{{ url_for('static', path='/css/tokens.css') }}" />
<link rel="stylesheet" href="{{ url_for('static', path='/css/layout.css') }}" />
<link rel="stylesheet" href="{{ url_for('static', path='/css/panels.css') }}" />
<link rel="stylesheet" href="{{ url_for('static', path='/css/dashboard.css') }}" />
<link rel="stylesheet" href="{{ url_for('static', path='/css/portfolio.css') }}" />
<link rel="stylesheet" href="{{ url_for('static', path='/css/log-chat.css') }}" />
<link rel="stylesheet" href="{{ url_for('static', path='/css/auth.css') }}" />
<link rel="stylesheet" href="{{ url_for('static', path='/css/settings.css') }}" />
<link rel="stylesheet" href="{{ url_for('static', path='/css/news.css') }}" />
<script src="{{ url_for('static', path='/js/htmx.min.js') }}" defer></script>
<link rel="stylesheet" href="{{ url_for('static', path='/css/tokens.css') }}?v={{ ASSET_VERSION }}" />
<link rel="stylesheet" href="{{ url_for('static', path='/css/layout.css') }}?v={{ ASSET_VERSION }}" />
<link rel="stylesheet" href="{{ url_for('static', path='/css/panels.css') }}?v={{ ASSET_VERSION }}" />
<link rel="stylesheet" href="{{ url_for('static', path='/css/dashboard.css') }}?v={{ ASSET_VERSION }}" />
<link rel="stylesheet" href="{{ url_for('static', path='/css/portfolio.css') }}?v={{ ASSET_VERSION }}" />
<link rel="stylesheet" href="{{ url_for('static', path='/css/log-chat.css') }}?v={{ ASSET_VERSION }}" />
<link rel="stylesheet" href="{{ url_for('static', path='/css/auth.css') }}?v={{ ASSET_VERSION }}" />
<link rel="stylesheet" href="{{ url_for('static', path='/css/settings.css') }}?v={{ ASSET_VERSION }}" />
<link rel="stylesheet" href="{{ url_for('static', path='/css/news.css') }}?v={{ ASSET_VERSION }}" />
<script src="{{ url_for('static', path='/js/htmx.min.js') }}?v={{ ASSET_VERSION }}" defer></script>
<script>
// Inject the user's preferred TONE (NOVICE | INTERMEDIATE) into every
// HTMX request so AI-generated panels resolve to the right cached

View file

@ -102,9 +102,9 @@
</div>
</div>
</section>
<script src="{{ url_for('static', path='/js/portfolio-sync.js') }}" defer></script>
<script src="{{ url_for('static', path='/js/portfolio.js') }}" defer></script>
<script src="{{ url_for('static', path='/js/portfolio_edit.js') }}" defer></script>
<script src="{{ url_for('static', path='/js/portfolio-sync.js') }}?v={{ ASSET_VERSION }}" defer></script>
<script src="{{ url_for('static', path='/js/portfolio.js') }}?v={{ ASSET_VERSION }}" defer></script>
<script src="{{ url_for('static', path='/js/portfolio_edit.js') }}?v={{ ASSET_VERSION }}" defer></script>
<section id="log-panel" class="panel">
<div class="panel-header">

View file

@ -27,10 +27,10 @@
<section class="shot-hero">
<button class="shot shot--hero"
data-full="{{ url_for('static', path='/images/dashboard.png') }}"
data-full="{{ url_for('static', path='/images/dashboard.png') }}?v={{ ASSET_VERSION }}"
data-alt="Read the Markets dashboard"
data-caption="The dashboard. An aggregate cross-asset read at the top, hand-picked indicator groups underneath. Reading level toggle (Novice / Intermediate) flips every AI-generated panel between plain-English and terse-pro framing.">
<img src="{{ url_for('static', path='/images/dashboard.png') }}"
<img src="{{ url_for('static', path='/images/dashboard.png') }}?v={{ ASSET_VERSION }}"
alt="Dashboard preview" loading="lazy">
<span class="shot__zoom" aria-hidden="true">Click to enlarge</span>
</button>
@ -48,10 +48,10 @@
off-hours stay quiet.
</p>
<button class="shot feature-card__shot"
data-full="{{ url_for('static', path='/images/news-feed.png') }}"
data-full="{{ url_for('static', path='/images/news-feed.png') }}?v={{ ASSET_VERSION }}"
data-alt="News feed with auto-tagged headlines"
data-caption="The news feed. Each headline carries one or more theme tags (rates, AI, energy, geopolitics, …) so you can keep the threads you care about and mute the ones you don't. Click a tag to include; shift-click to exclude.">
<img src="{{ url_for('static', path='/images/news-feed.png') }}"
<img src="{{ url_for('static', path='/images/news-feed.png') }}?v={{ ASSET_VERSION }}"
alt="News feed thumbnail" loading="lazy">
</button>
</div>
@ -66,10 +66,10 @@
in earnings, policy, valuation &mdash; not chart patterns.
</p>
<button class="shot feature-card__shot"
data-full="{{ url_for('static', path='/images/indicators-read.png') }}"
data-full="{{ url_for('static', path='/images/indicators-read.png') }}?v={{ ASSET_VERSION }}"
data-alt="Indicators panel with AI commentary"
data-caption="The indicators panel. Tabs across asset classes (equity, rates, commodities, FX, bonds, …); each tab carries a one-paragraph 'read' written by the model on top of the live prices. The numbers anchor the prose so the commentary is checkable, not floating.">
<img src="{{ url_for('static', path='/images/indicators-read.png') }}"
<img src="{{ url_for('static', path='/images/indicators-read.png') }}?v={{ ASSET_VERSION }}"
alt="Indicators panel thumbnail" loading="lazy">
</button>
</div>
@ -87,10 +87,10 @@
not a forecast and not advice on any investment decision.
</p>
<button class="shot feature-card__shot"
data-full="{{ url_for('static', path='/images/strategic-log.png') }}"
data-full="{{ url_for('static', path='/images/strategic-log.png') }}?v={{ ASSET_VERSION }}"
data-alt="Strategic log — the editorial AI read"
data-caption="The strategic log. The model writes a fresh interpretation through the trading day, taking the previous draft as context so it updates rather than starts over. Sections are typed: date header, TL;DR, what moved, what to watch, system temperature. Paid users get a refresh every hour; free users get one every six.">
<img src="{{ url_for('static', path='/images/strategic-log.png') }}"
<img src="{{ url_for('static', path='/images/strategic-log.png') }}?v={{ ASSET_VERSION }}"
alt="Strategic log thumbnail" loading="lazy">
</button>
</div>
@ -100,10 +100,10 @@
<h2 class="public-section__head">More views</h2>
<div class="shots-grid">
<button class="shot"
data-full="{{ url_for('static', path='/images/chat-with-log.png') }}"
data-full="{{ url_for('static', path='/images/chat-with-log.png') }}?v={{ ASSET_VERSION }}"
data-alt="Ask follow-up questions against any past log"
data-caption="Ask follow-up questions against any past log. The chat panel inherits the log's full context, so you can pull on a thread without re-pasting headlines or re-explaining the setup.">
<img src="{{ url_for('static', path='/images/chat-with-log.png') }}"
<img src="{{ url_for('static', path='/images/chat-with-log.png') }}?v={{ ASSET_VERSION }}"
alt="Chat-with-log thumbnail" loading="lazy">
<div class="shot__caption">
<strong>Ask anything about a log</strong>

View file

@ -69,5 +69,5 @@
{% endif %}
</div>
</section>
{% if paid %}<script src="{{ url_for('static', path='/js/chat.js') }}" defer></script>{% endif %}
{% if paid %}<script src="{{ url_for('static', path='/js/chat.js') }}?v={{ ASSET_VERSION }}" defer></script>{% endif %}
{% endblock %}

View file

@ -10,9 +10,9 @@
catch (e) { document.documentElement.dataset.theme = 'light'; }
})();
</script>
<link rel="stylesheet" href="{{ url_for('static', path='/css/tokens.css') }}" />
<link rel="stylesheet" href="{{ url_for('static', path='/css/layout.css') }}" />
<link rel="stylesheet" href="{{ url_for('static', path='/css/auth.css') }}" />
<link rel="stylesheet" href="{{ url_for('static', path='/css/tokens.css') }}?v={{ ASSET_VERSION }}" />
<link rel="stylesheet" href="{{ url_for('static', path='/css/layout.css') }}?v={{ ASSET_VERSION }}" />
<link rel="stylesheet" href="{{ url_for('static', path='/css/auth.css') }}?v={{ ASSET_VERSION }}" />
</head>
<body>
<div class="auth-shell">

View file

@ -14,12 +14,12 @@
} catch (e) { document.documentElement.dataset.theme = 'light'; }
})();
</script>
<link rel="stylesheet" href="{{ url_for('static', path='/css/tokens.css') }}" />
<link rel="stylesheet" href="{{ url_for('static', path='/css/layout.css') }}" />
<link rel="stylesheet" href="{{ url_for('static', path='/css/panels.css') }}" />
<link rel="stylesheet" href="{{ url_for('static', path='/css/auth.css') }}" />
<link rel="stylesheet" href="{{ url_for('static', path='/css/settings.css') }}" />
<link rel="stylesheet" href="{{ url_for('static', path='/css/public.css') }}" />
<link rel="stylesheet" href="{{ url_for('static', path='/css/tokens.css') }}?v={{ ASSET_VERSION }}" />
<link rel="stylesheet" href="{{ url_for('static', path='/css/layout.css') }}?v={{ ASSET_VERSION }}" />
<link rel="stylesheet" href="{{ url_for('static', path='/css/panels.css') }}?v={{ ASSET_VERSION }}" />
<link rel="stylesheet" href="{{ url_for('static', path='/css/auth.css') }}?v={{ ASSET_VERSION }}" />
<link rel="stylesheet" href="{{ url_for('static', path='/css/settings.css') }}?v={{ ASSET_VERSION }}" />
<link rel="stylesheet" href="{{ url_for('static', path='/css/public.css') }}?v={{ ASSET_VERSION }}" />
</head>
<body class="public-page">
<div class="public-shell">

View file

@ -357,7 +357,7 @@
{% if user %}
{# Import widget wiring — auto-parse on drop, preview, then commit. #}
<script src="{{ url_for('static', path='/js/portfolio.js') }}" defer></script>
<script src="{{ url_for('static', path='/js/settings-import.js') }}" defer></script>
<script src="{{ url_for('static', path='/js/portfolio.js') }}?v={{ ASSET_VERSION }}" defer></script>
<script src="{{ url_for('static', path='/js/settings-import.js') }}?v={{ ASSET_VERSION }}" defer></script>
{% endif %}
{% endblock %}

View file

@ -10,9 +10,9 @@
catch (e) { document.documentElement.dataset.theme = 'light'; }
})();
</script>
<link rel="stylesheet" href="{{ url_for('static', path='/css/tokens.css') }}" />
<link rel="stylesheet" href="{{ url_for('static', path='/css/layout.css') }}" />
<link rel="stylesheet" href="{{ url_for('static', path='/css/auth.css') }}" />
<link rel="stylesheet" href="{{ url_for('static', path='/css/tokens.css') }}?v={{ ASSET_VERSION }}" />
<link rel="stylesheet" href="{{ url_for('static', path='/css/layout.css') }}?v={{ ASSET_VERSION }}" />
<link rel="stylesheet" href="{{ url_for('static', path='/css/auth.css') }}?v={{ ASSET_VERSION }}" />
</head>
<body>
<div class="auth-shell">

View file

@ -3,6 +3,7 @@ Imported by both routers/pages.py and routers/api.py so the filters are
registered exactly once."""
from __future__ import annotations
import time
from pathlib import Path
from fastapi.templating import Jinja2Templates
@ -13,6 +14,13 @@ from app.config import get_settings
from app.services.glossary import wrap_glossary
# Cache-busting token for static assets. Computed once at import time
# (i.e. process startup), so every container restart yields a fresh
# value and browsers refetch CSS/JS instead of serving stale cache.
# Templates append `?v={{ ASSET_VERSION }}` to every static URL.
ASSET_VERSION = str(int(time.time()))
TEMPLATE_DIR = Path(__file__).resolve().parent / "templates"
@ -77,3 +85,4 @@ templates.env.globals["LEGAL_OPERATOR"] = branding.LEGAL_OPERATOR
templates.env.globals["OPERATOR_EMAIL"] = branding.OPERATOR_EMAIL
templates.env.globals["OPERATOR_JURISDICTION"] = branding.OPERATOR_JURISDICTION
templates.env.globals["BETA_MODE"] = get_settings().BETA_MODE
templates.env.globals["ASSET_VERSION"] = ASSET_VERSION