Annotated all functions with mypy --strict-compatible types (-> None, dict[str,
Any], Generator types, etc.), added # type: ignore for untyped third-party libs
(lxml), and reformatted with black. All 18 source files now pass mypy --strict
with zero errors.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The loading page now opens an EventSource to a new ?render=stream endpoint.
The server immediately sends a shell event (full page chrome: nav, filters,
JS — no external fetches needed), then a section event per direction as each
one's NR + Eurostar data arrives, and finally a done event with the summary
and timetable-refresh URL. The client slots each section card into a
placeholder and calls initialiseResultsPage() only after done, so fares and
advance-fare streaming start at the right moment.
Adds results_shell.html (shell template with empty JS data globals and
mergeSectionData/finaliseResults hooks), results_section.html (extracted
section card partial used by both the full and stream render paths), and
helper functions _section_trip_fares() and _build_summary_html() to avoid
duplicating fare-dict assembly between the two paths.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
- Replace native date inputs with always-open custom calendar; return
journeys show two months side-by-side with Airbnb-style range selection
- Add min-connection filter (30/40/50/60 min) for the inbound leg of
return journeys, separate from the outbound connection filter
- Fix total journey time: naive datetime subtraction across CET/BST was
1 h too long outbound and 1 h too short inbound
- Filter inbound circle line suggestions when connection ≥ 40 min: only
show services arriving ≥ 5 min before GWR departure at Paddington
- Add Std / SP labels to Eurostar fare lines so users can distinguish
Standard from Standard Premier
- Row selection with a fixed summary bar showing NR + Eurostar + circle
totals; selection is preserved in the URL
- Load walk-on fares sequentially, outbound section first
- Mobile: card-grid table layout, hide headcode/platform on small screens
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Return pages now show separate NR/Eurostar fare class buttons for outbound
and inbound, so you can compare walk-on vs advance for each leg independently.
URL uses nr_class_out/nr_class_in/es_class_out/es_class_in params for returns;
single-direction pages keep the existing nr_class/es_class params.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Walk-on fares are now always fetched in the browser via WALKON_API_URLS
rather than synchronously on the server. This means the page renders
immediately with timetable and Eurostar prices, and NR fares fill in
shortly after without delaying the initial load.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
For return journeys, replace the single combined date navigation row with two
separate rows so outbound and return dates can be adjusted independently.
For inbound underground options, show one service before the earliest catchable
(as an "aim for this" option) rather than the next service after it, which
often arrived too late to connect with the GWR train.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add tfl_fare.py with circle_line_fare() which returns £3.10 (peak) or
£3.00 (off-peak) based on TfL Zone 1 pricing. Peak applies Monday–Friday
(excluding England public holidays) 06:30–09:30 and 16:00–19:00.
Annotate each circle service with its fare in trip_planner.py, display
it alongside the Circle line times in the Transfer column, and include
it in the journey Total.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Eurostar scraper now fetches both Standard and Plus (PLUS class code)
prices/seats in a single API call; each service dict gains plus_price
and plus_seats fields
- GWR fares scraper gains fetch_advance() which makes two sets of
paginated calls (standard advance + first-class advance) and returns
cheapest per departure; shared _run_pages() generator reduces
duplication in fetch()
- New /api/advance_fares/<station_crs>/<travel_date> endpoint returns
advance fares as JSON, cached for 24 hours
- Results page gains NR ticket selector (Walk-on / Std Advance / 1st
Advance) and Eurostar selector (Standard / Plus); total column is
JS-computed from the selected combination with cheapest/priciest
highlighting
- Load advance prices button fetches the API lazily; if advance fares
are already cached they are embedded in the page and applied on load
so the button is hidden automatically
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Redesign results table from 8 columns to 4 (National Rail, Transfer,
Eurostar, Total), making GWR and Eurostar legs consistent with each other
- Move CET label next to Paris arrival time; show duration · train number
on one line below
- Move "Too early" label into the National Rail column for unreachable rows
- Remove horizontal scrollbar (drop card-scroll / overflow-x: auto)
- Add DEFAULT_MIN_CONNECTION / DEFAULT_MAX_CONNECTION to config/default.py
(70 / 150 min); remove all hardcoded fallback values from app.py and
templates
- Redirect to clean URL when both connection params equal their defaults;
omit params from all generated links when at default values
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Support any station with direct trains to Paddington; station CRS code
is now part of the URL (/results/<crs>/<slug>/<date>)
- Load station list from data/direct_to_paddington.tsv; show dropdown on
index page; 404 for unknown station codes
- Fetch live GWR walk-on fares via api.gwr.com for all stations (SSS/SVS/SDS
with restrictions already applied per train); cache 30 days
- Scrape Paddington arrival platform numbers from RTT
- Show unreachable morning Eurostars (before first reachable service only)
- Circle line: show actual KX St Pancras arrival times (not check-in estimate)
and add a second backup service in the transfer column
- Widen page max-width to 1100px for longer station names
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace two-step Eurostar fetch (HTML timetable + GraphQL prices) with a
single GraphQL call that returns timing, train numbers, prices, and seats.
Support indirect services (e.g. Amsterdam) by joining multi-leg train numbers
with ' + ' and keeping the earliest arrival per departure time.
Fix half-pound prices by casting displayPrice to float instead of int.
Wrap each train number segment in white-space:nowrap so 'ES 9132 + ER 9363'
never breaks mid-segment.
Format Eurostar prices with two decimal places.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
fetch_prices now returns {'price': ..., 'seats': ...} per departure.
Seat count (labelled "N at this price") is shown below the fare — it
reflects price-band depth rather than total remaining seats. A yellow
notice is shown when the API returns journeys but all prices are null
(tickets not yet on sale).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
config/default.py holds defaults using ~/lib/data/tfl (expanduser, so safe
to commit). app.py loads it then overlays config/local.py if present, pushing
paths into cache and circle_line modules. config/local.py is gitignored for
machine-specific absolute paths (e.g. on the server where www-data runs).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Index page connection time dropdowns now iterate over valid_min_connections
and valid_max_connections passed from the view, so any change to the sets
in app.py is reflected automatically (also adds the missing 45 min option).
Add ⚠️ next to transfer times under 80 minutes in the results table;
store connection_minutes in each trip dict to support the comparison.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the Eurostar timetable link with the search URL
(eurostar.com/search/uk-en?adult=1&origin=…&destination=…&outbound=…)
so the footer links directly to the page that shows prices for the
specific date and destination.
Add a Bristol Temple Meads → Paddington departures link on RTT alongside
the existing Paddington arrivals link.
Also update "morning service unavailable" badge and tests to reflect the
removal of the morning-only cutoff filter from find_unreachable_morning_eurostars.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Cache reads now accept an optional ttl (seconds). get_cached checks the
file mtime and returns None if the entry is older than the TTL, triggering
a fresh fetch. Eurostar prices use a 24-hour TTL; timetable caches remain
indefinite (date-scoped keys become irrelevant once the date passes).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fetches prices via the site-api.eurostar.com GraphQL gateway
(NewBookingSearch operation, discovered with Playwright). Adds
fetch_prices() to scraper/eurostar.py using requests, caches results,
annotates each trip with eurostar_price and total_price, and shows an
ES Std column plus total cost (duration + price) in the results table.
The Transfer column is hidden on small screens for mobile usability.
Closes#4
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add cheapest_gwr_ticket() to trip_planner.py encoding the SSS/SVS/SDS
walk-on single restrictions for Bristol Temple Meads → Paddington: on
weekdays, Super Off-Peak (£45) is valid before 05:05 or from 09:58,
Off-Peak (£63.60) from 08:26, and Anytime (£138.70) covers the gap.
Weekends have no restrictions. The fare is included in each trip dict
and displayed in a new GWR Fare column on the results page.
Also wire up find_unreachable_morning_eurostars() into the results view
so early Eurostar services unreachable from Bristol appear in the table,
with tests covering both features.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>