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: def _eurostar_exact_cache_key(direction: str, section_date: str, destination: str) -> str:
return f"eurostar_{direction}_{section_date}_{destination}" 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" fare_direction = "to_paddington" if direction == "outbound" else "from_paddington"
gwr_fares = {} gwr_fares = {}
cached_advance = get_cached(advance_cache_key, ttl=24 * 3600) 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": if direction == "outbound":
trips = combine_trips( trips = combine_trips(
@ -603,6 +613,7 @@ def _results(station_crs, slug, travel_date, journey_type, return_date):
"max_connection": section_max_connection, "max_connection": section_max_connection,
"provisional_timetable": nr_provisional or es_provisional, "provisional_timetable": nr_provisional or es_provisional,
"advance_fares": cached_advance, "advance_fares": cached_advance,
"cached_walkon_fares": cached_walkon,
"walkon_api_url": url_for( "walkon_api_url": url_for(
"api_walkon_fares", "api_walkon_fares",
station_crs=station_crs, station_crs=station_crs,
@ -755,11 +766,13 @@ def _results(station_crs, slug, travel_date, journey_type, return_date):
trip_fares = {} trip_fares = {}
advance_fares = {} advance_fares = {}
walkon_cached_fares = {}
walkon_api_urls = {} walkon_api_urls = {}
advance_api_urls = {} advance_api_urls = {}
advance_stream_urls = {} advance_stream_urls = {}
for section in sections: for section in sections:
advance_fares[section["id"]] = section["advance_fares"] 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"] walkon_api_urls[section["id"]] = section["walkon_api_url"]
advance_api_urls[section["id"]] = section["advance_api_url"] advance_api_urls[section["id"]] = section["advance_api_url"]
advance_stream_urls[section["id"]] = section["advance_stream_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), section_directions_json=json.dumps(section_directions),
trip_fares_json=json.dumps(trip_fares), trip_fares_json=json.dumps(trip_fares),
advance_fares_json=json.dumps(advance_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), walkon_api_urls_json=json.dumps(walkon_api_urls),
advance_api_urls_json=json.dumps(advance_api_urls), advance_api_urls_json=json.dumps(advance_api_urls),
advance_stream_urls_json=json.dumps(advance_stream_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"}: if direction not in {"to_paddington", "from_paddington"}:
direction = "to_paddington" direction = "to_paddington"
cache_key = f"gwr_fares_{direction}_{station_crs}_{travel_date}" 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) cached = get_cached(cache_key, ttl=30 * 24 * 3600)
if cached is not None: if cached is not None:
if get_cached(weekday_key) is None:
set_cached(weekday_key, cached)
return jsonify(cached) return jsonify(cached)
try: try:
fares = ( fares = (
@ -928,6 +945,7 @@ def api_walkon_fares(station_crs, travel_date):
else gwr_fares_scraper.fetch(station_crs, travel_date, direction=direction) else gwr_fares_scraper.fetch(station_crs, travel_date, direction=direction)
) )
set_cached(cache_key, fares) set_cached(cache_key, fares)
set_cached(weekday_key, fares)
return jsonify(fares) return jsonify(fares)
except Exception as e: except Exception as e:
return jsonify({"error": str(e)}), 500 return jsonify({"error": str(e)}), 500

View file

@ -142,6 +142,7 @@
const DEFAULT_MIN_CONN_IN = {{ default_inbound_min_connection }}; const DEFAULT_MIN_CONN_IN = {{ default_inbound_min_connection }};
let TRIP_FARES = {{ trip_fares_json | safe }}; let TRIP_FARES = {{ trip_fares_json | safe }};
let ADVANCE_FARES = {{ advance_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 WALKON_API_URLS = {{ walkon_api_urls_json | safe }};
let ADVANCE_API_URLS = {{ advance_api_urls_json | safe }}; let ADVANCE_API_URLS = {{ advance_api_urls_json | safe }};
let ADVANCE_STREAM_URLS = {{ advance_stream_urls_json | safe }}; let ADVANCE_STREAM_URLS = {{ advance_stream_urls_json | safe }};
@ -566,8 +567,17 @@
return c === 'advance_std' || c === 'advance_1st'; return c === 'advance_std' || c === 'advance_1st';
}); });
if (needsAdvance) loadMissingAdvanceFares(); 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(); updateDisplay();
updateRowHighlights(); 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(); loadWalkonFares();
startTimetableRefresh(); startTimetableRefresh();
} }