Commit graph

65 commits

Author SHA1 Message Date
ed8a5626a4 Refine homepage journey form layout 2026-05-26 12:55:23 +01:00
13c4341f3a Add full type annotations and black formatting across all modules
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>
2026-05-25 21:48:53 +01:00
453d6244ec Stream results progressively via SSE instead of waiting for full render
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>
2026-05-25 21:24:40 +01:00
5f0d2c71b1 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>
2026-05-25 16:51:11 +01:00
1bc7631863 Five UI and data features for return journeys and results page
- 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>
2026-05-25 14:58:32 +01:00
a88b19fa4c Add (CET) label next to inbound Eurostar departure time
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 20:27:09 +01:00
298ce14812 Add per-direction NR ticket and Eurostar class selectors for return journeys
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>
2026-05-21 20:17:57 +01:00
8d998aad71 Sticky table headers while scrolling within the table
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 18:49:00 +01:00
2f554b9ca0 Relabel inbound circle line services: Circle (aim for) and next (fallback)
Consistent with outbound labelling. The first service is the one to aim
for if the Eurostar arrives slightly early; the second is dimmed as it
may not leave enough time for the GWR connection.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 18:48:14 +01:00
5910bae33b Show spinner while walk-on fares are loading
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 18:42:01 +01:00
03d2a53961 Stream GWR walk-on fares client-side instead of blocking page render
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>
2026-05-21 18:40:11 +01:00
b184e27a63 Show per-direction service counts for return journeys
Replace the combined "70 NR · 36 Eurostar" summary with separate
outbound/return lines so it's clear which counts belong to which leg.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 14:47:33 +01:00
4194e8fa64 Improve return date UX on search form
Disable the return date input until Return journey type is selected.
Clicking anywhere in the return date group auto-selects Return and
enables the field. The return date min is kept in sync with the
outbound date, bumping the value forward if it would otherwise fall
before the outbound date.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 14:42:39 +01:00
a859b96a23 Split return date nav into separate outbound/return rows; show earlier tube option on inbound
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>
2026-05-21 14:09:36 +01:00
1407cb8246 Hide Eurostar note during provisional loads 2026-05-21 12:27:15 +01:00
06e622d817 Refresh provisional results in place 2026-05-21 12:04:53 +01:00
bc7cb9cffa Cache provisional weekday timetables 2026-05-21 11:31:17 +01:00
378d2484d0 Improve progressive results loading 2026-05-21 09:52:58 +01:00
9691632f65 Add return and inbound journey support 2026-05-21 08:46:35 +01:00
6ba71447ef Add Codex marker file 2026-05-20 15:53:07 +01:00
a5023d0672 Stream advance fares and selectable ticket classes 2026-05-20 15:52:53 +01:00
2f3f01171c Add travel fare notes 2026-05-20 15:52:50 +01:00
0885019136 Add FastCGI entrypoint 2026-05-20 15:52:47 +01:00
9525059829 Add favicon asset 2026-05-20 15:52:45 +01:00
b38951a6a5 Use url_for for advance fares API URL
Hardcoded path broke when deployed under a subpath such as
/paddington-eurostar/; generate the URL server-side with url_for instead.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-11 16:51:35 +01:00
35097fda4f Add Circle line fare to Transfer column and Total
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>
2026-04-11 16:47:34 +01:00
89a536dfd3 Add Eurostar Plus prices and NR advance fare support
- 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>
2026-04-11 16:22:24 +01:00
5583a20143 Add station sub-labels to results table column headers
Show the through-route in each header: National Rail (origin → Paddington),
Transfer (Paddington → St Pancras), Eurostar (St Pancras → destination).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 13:21:25 +01:00
8433252cae Tidy results table layout and centralise connection defaults
- 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>
2026-04-10 13:11:26 +01:00
6ab4534051 Rename tool 2026-04-09 15:40:32 +01:00
ad1aa57c00 Adjust default connection time. 2026-04-09 15:40:15 +01:00
b487307d4a Add CDS (Off-Peak Day Single) to wanted fare codes
Fixes #5 — Goring & Streatley (GOR) was only showing Anytime Day Single
because ticket code CDS was not included in _WANTED_CODES.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 15:18:22 +01:00
4900be723b Remove some starting stations. 2026-04-07 14:37:03 +01:00
2440fb3f02 Add GWR fare API sample responses for BRI, CDF, OXF, PLY
Reference JSON showing ticket types returned by the GWR journey
search API for four representative stations: Bristol Temple Meads
(SSS/SVS/SDS), Cardiff Central (SVS/SDS), Oxford (CDS/SDS), and
Plymouth (SVS/SDS). Documents that no restriction codes are exposed
in the API response.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 14:28:11 +01:00
3c787b33d3 Add multi-station support, GWR fares API, and Circle line improvements
- 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>
2026-04-06 20:22:44 +01:00
71be0dd8cf Move inline styles to CSS classes; update README
Extract repeated inline styles from templates into named CSS classes in
base.html: layout helpers, buttons, form groups, alert boxes, results table
rules, row highlight classes, typography utilities, and empty-state styles.
Remove the per-page <style> block from results.html.

Update README to reflect current destinations, GraphQL data source, Circle
Line timetable, configurable connection range, and GWR fare table.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 15:39:04 +01:00
e6f310f517 Add Cologne Hbf destination; use coin emoji for cheapest journey
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 15:02:01 +01:00
c22a3ea0fc Consolidate to single GraphQL call; show indirect trains; fix price formatting
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>
2026-04-04 14:46:22 +01:00
05eec29b7d Show Eurostar seat availability and no-prices notice
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>
2026-04-04 14:12:54 +01:00
cd37f0619b Add config system with TFL_DATA_DIR and CACHE_DIR
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>
2026-04-04 13:37:44 +01:00
c215456620 Use real Circle Line timetable; add Eurostar duration
Parse Circle Line times from TransXChange XML (output_txc_01CIR_.xml) with
separate weekday/Saturday/Sunday schedules, replacing the approximated
every-10-minutes pattern. Subtract 1 hour timezone offset (CET/CEST vs
GMT/BST) when computing Eurostar journey duration, shown for both viable
and unreachable services.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 13:26:35 +01:00
60674fe663 Add Circle Line timetable info. 2026-04-04 12:58:19 +01:00
cdee44ea3f Adjust wording, add nowrap. 2026-04-04 12:45:43 +01:00
d089d3d716 Add price emoji indicators and hover text to results table
Show 💰 on total prices within £10 of the cheapest journey and 💸
within £10 of the most expensive, mirroring the /🐢 logic for journey
time. Only applied when more than one priced trip exists.

Add title attributes to ⚠️ ("Tight connection"),  ("Fastest journey"),
and 🐢 ("Slowest journey") for accessibility and discoverability.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 12:28:12 +01:00
e7695a5e49 Drive search form dropdowns from VALID_MIN/MAX_CONNECTIONS; warn on short transfers
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>
2026-04-04 10:52:32 +01:00
19656f412a Prevent GWR duration from wrapping on small screens
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 10:48:49 +01:00
06afd57957 Link to Eurostar search page with pricing; add Bristol departures RTT link
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>
2026-04-04 10:47:46 +01:00
4e4202e220 Drop MORNING_CUTOFF_HOUR, consider all services. 2026-04-04 10:43:47 +01:00
df94e822ae Try 45 minute min connection time. 2026-04-04 10:43:31 +01:00
6b044b9493 Add 24-hour TTL to Eurostar price cache
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>
2026-04-04 10:38:47 +01:00