Cache walk-on NR fares by day of week for instant display

Store GWR walk-on fares keyed by timetable period + weekday
(weekday_gwr_fares_{direction}_{crs}_{period}_{day}), mirroring
the existing NR timetable weekday cache strategy.

On page load the server embeds any cached weekday fares in the page
as WALKON_CACHED_FARES so JS can populate prices immediately without
waiting for the GWR API. The live API call still runs afterwards to
verify and update any changed fares; the spinner label changes to
"Verifying fares" when cached prices are already shown.

The weekday cache is written whenever exact-date fares are fetched
from GWR, keeping it fresh, and populated lazily from the exact-date
cache on first access.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Edward Betts 2026-05-25 16:51:11 +01:00
parent 1bc7631863
commit 5f0d2c71b1
2 changed files with 28 additions and 0 deletions

18
app.py
View file

@ -129,6 +129,13 @@ def _nr_weekday_cache_key(direction: str, station_crs: str, section_date: str) -
)
def _walkon_weekday_cache_key(direction: str, station_crs: str, section_date: str) -> str:
return (
f"weekday_gwr_fares_{direction}_{station_crs}_"
f"{_nr_timetable_period_key(section_date)}_{_weekday_for(section_date)}"
)
def _eurostar_exact_cache_key(direction: str, section_date: str, destination: str) -> str:
return f"eurostar_{direction}_{section_date}_{destination}"
@ -517,6 +524,9 @@ def _results(station_crs, slug, travel_date, journey_type, return_date):
fare_direction = "to_paddington" if direction == "outbound" else "from_paddington"
gwr_fares = {}
cached_advance = get_cached(advance_cache_key, ttl=24 * 3600)
walkon_weekday_key = _walkon_weekday_cache_key(rtt_direction, station_crs, section_date)
exact_walkon = get_cached(gwr_cache_key, ttl=30 * 24 * 3600)
cached_walkon = exact_walkon if exact_walkon is not None else get_cached(walkon_weekday_key)
if direction == "outbound":
trips = combine_trips(
@ -603,6 +613,7 @@ def _results(station_crs, slug, travel_date, journey_type, return_date):
"max_connection": section_max_connection,
"provisional_timetable": nr_provisional or es_provisional,
"advance_fares": cached_advance,
"cached_walkon_fares": cached_walkon,
"walkon_api_url": url_for(
"api_walkon_fares",
station_crs=station_crs,
@ -755,11 +766,13 @@ def _results(station_crs, slug, travel_date, journey_type, return_date):
trip_fares = {}
advance_fares = {}
walkon_cached_fares = {}
walkon_api_urls = {}
advance_api_urls = {}
advance_stream_urls = {}
for section in sections:
advance_fares[section["id"]] = section["advance_fares"]
walkon_cached_fares[section["id"]] = section.get("cached_walkon_fares")
walkon_api_urls[section["id"]] = section["walkon_api_url"]
advance_api_urls[section["id"]] = section["advance_api_url"]
advance_stream_urls[section["id"]] = section["advance_stream_url"]
@ -860,6 +873,7 @@ def _results(station_crs, slug, travel_date, journey_type, return_date):
section_directions_json=json.dumps(section_directions),
trip_fares_json=json.dumps(trip_fares),
advance_fares_json=json.dumps(advance_fares),
walkon_cached_fares_json=json.dumps(walkon_cached_fares),
walkon_api_urls_json=json.dumps(walkon_api_urls),
advance_api_urls_json=json.dumps(advance_api_urls),
advance_stream_urls_json=json.dumps(advance_stream_urls),
@ -918,8 +932,11 @@ def api_walkon_fares(station_crs, travel_date):
if direction not in {"to_paddington", "from_paddington"}:
direction = "to_paddington"
cache_key = f"gwr_fares_{direction}_{station_crs}_{travel_date}"
weekday_key = _walkon_weekday_cache_key(direction, station_crs, travel_date)
cached = get_cached(cache_key, ttl=30 * 24 * 3600)
if cached is not None:
if get_cached(weekday_key) is None:
set_cached(weekday_key, cached)
return jsonify(cached)
try:
fares = (
@ -928,6 +945,7 @@ def api_walkon_fares(station_crs, travel_date):
else gwr_fares_scraper.fetch(station_crs, travel_date, direction=direction)
)
set_cached(cache_key, fares)
set_cached(weekday_key, fares)
return jsonify(fares)
except Exception as e:
return jsonify({"error": str(e)}), 500

View file

@ -142,6 +142,7 @@
const DEFAULT_MIN_CONN_IN = {{ default_inbound_min_connection }};
let TRIP_FARES = {{ trip_fares_json | safe }};
let ADVANCE_FARES = {{ advance_fares_json | safe }};
const WALKON_CACHED_FARES = {{ walkon_cached_fares_json | safe }};
let WALKON_API_URLS = {{ walkon_api_urls_json | safe }};
let ADVANCE_API_URLS = {{ advance_api_urls_json | safe }};
let ADVANCE_STREAM_URLS = {{ advance_stream_urls_json | safe }};
@ -566,8 +567,17 @@
return c === 'advance_std' || c === 'advance_1st';
});
if (needsAdvance) loadMissingAdvanceFares();
/* Pre-populate walk-on fares from weekday cache so prices show immediately */
var hasPreloaded = false;
for (var sid in WALKON_CACHED_FARES) {
if (WALKON_CACHED_FARES[sid]) { mergeWalkonFares(sid, WALKON_CACHED_FARES[sid]); hasPreloaded = true; }
}
updateDisplay();
updateRowHighlights();
if (hasPreloaded) {
var loadingEl = document.getElementById('walkon-loading');
if (loadingEl) loadingEl.innerHTML = '<span class="spinner spinner-inline" aria-hidden="true"></span>Verifying fares';
}
loadWalkonFares();
startTimetableRefresh();
}