diff --git a/README.md b/README.md index 5a53c28..ef56b9b 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@ Source: https://git.4angle.com/edward/bristol-eurostar -Plan a trip from Bristol Temple Meads to Europe on Eurostar. +Plan a trip from Bristol Temple Meads to Europe via Eurostar. -Combines GWR trains (Bristol → Paddington) with Eurostar services (St Pancras → destination) and shows all valid same-day connections, filtering by journey time and minimum/maximum transfer window at Paddington/St Pancras. +Combines GWR trains (Bristol → Paddington) with Eurostar services (St Pancras → destination) and shows all valid same-day connections, including Circle Line times for the Paddington → St Pancras transfer. Displays GWR walk-on fares, Eurostar Standard prices, seat availability, and total journey cost. ## Destinations @@ -12,23 +12,42 @@ Combines GWR trains (Bristol → Paddington) with Eurostar services (St Pancras - Brussels Midi - Lille Europe - Amsterdam Centraal +- Rotterdam Centraal +- Cologne Hbf ## How it works -Train times are fetched from two sources simultaneously: +Train times and prices are fetched from two sources: - **GWR** — scraped from [Realtime Trains](https://www.realtimetrains.co.uk/) using httpx -- **Eurostar** — scraped from the Eurostar timetable pages via the embedded `__NEXT_DATA__` JSON (no browser required) +- **Eurostar** — fetched from the Eurostar GraphQL API (single call returns timetable, Standard fares, and seat availability) + +The Paddington → St Pancras transfer uses a real Circle Line timetable parsed from a TfL TransXChange XML file, accounting for walk time to the platform at Paddington and walk time from the platform to the St Pancras check-in. Results are cached to disk by date and destination. ## Connection constraints +Configurable via the search form. Defaults: + | | | |---|---| -| Minimum Paddington → St Pancras | 75 min | -| Maximum Paddington → St Pancras | 2h 20m | -| Maximum Bristol → Paddington | 1h 50m | +| Minimum Paddington → St Pancras | 50 min | +| Maximum Paddington → St Pancras | 110 min | + +Valid range: 45–120 min (min), 60–180 min (max). + +## GWR fares + +Walk-on single fares for Bristol Temple Meads → Paddington, selected automatically by departure time: + +| Ticket | Price | Restriction (weekdays) | +|---|---|---| +| Super Off-Peak | £45.00 | Not valid 05:05–09:57 | +| Off-Peak | £63.60 | Not valid before 08:26 | +| Anytime | £138.70 | No restriction | + +Weekends always use Super Off-Peak. ## Setup @@ -36,6 +55,19 @@ Results are cached to disk by date and destination. pip install -e ".[dev]" ``` +### Configuration + +Copy or create `config/local.py` (gitignored) to override defaults: + +```python +CACHE_DIR = '/var/cache/bristol-eurostar' +CIRCLE_LINE_XML = '/path/to/output_txc_01CIR_.xml' +``` + +Defaults (in `config/default.py`) use `~/lib/data/tfl/`. + +The Circle Line XML is a TfL TransXChange timetable file. The Paddington (H&C Line) stop is `9400ZZLUPAH1`; the King's Cross St Pancras stop is `9400ZZLUKSX3`. + ## Running ```bash diff --git a/templates/base.html b/templates/base.html index aa65c78..5fb61b4 100644 --- a/templates/base.html +++ b/templates/base.html @@ -185,9 +185,103 @@ .card { padding: 1.25rem; } + .col-transfer { display: none; } } a { color: #00539f; } + + /* Card helpers */ + .card > h2:first-child { margin-top: 0; } + .card-scroll { overflow-x: auto; } + + /* Form groups */ + .form-group { margin-bottom: 1.2rem; } + .form-group-lg { margin-bottom: 1.5rem; } + + /* Buttons */ + .btn-primary { + background: #00539f; + color: #fff; + border: none; + padding: 0.75rem 2rem; + font-size: 1rem; + font-weight: 600; + border-radius: 4px; + cursor: pointer; + } + + .btn-nav { + padding: 0.3rem 0.75rem; + border: 1px solid #cbd5e0; + border-radius: 4px; + text-decoration: none; + color: #00539f; + font-size: 0.9rem; + } + + /* Inline select */ + .select-inline { + padding: 0.3rem 0.6rem; + font-size: 0.9rem; + border: 1px solid #cbd5e0; + border-radius: 4px; + } + + /* Alert boxes */ + .alert { + margin-top: 1rem; + padding: 0.75rem 1rem; + border-radius: 4px; + } + .alert-error { + background: #fff5f5; + border: 1px solid #fc8181; + color: #c53030; + } + .alert-warning { + background: #fffbeb; + border: 1px solid #f6e05e; + color: #744210; + } + + /* Results page layout */ + .back-link { margin-bottom: 1rem; } + .date-nav { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.5rem; } + .switcher-section { margin: 0.9rem 0 1rem; } + .section-label { font-size: 0.9rem; font-weight: 600; margin-bottom: 0.45rem; } + .filter-row { margin-top: 0.75rem; display: flex; gap: 1.5rem; align-items: center; } + .filter-label { font-size: 0.9rem; font-weight: 600; margin-right: 0.5rem; } + .card-meta { color: #4a5568; margin: 0; } + .footnote { margin-top: 1rem; font-size: 0.82rem; color: #718096; } + + /* Results table */ + .results-table { width: 100%; border-collapse: collapse; font-size: 0.95rem; } + .results-table th, + .results-table td { padding: 0.6rem 0.8rem; } + .results-table thead tr { border-bottom: 2px solid #e2e8f0; text-align: left; } + .results-table tbody tr { border-bottom: 1px solid #e2e8f0; } + .row-fast { background: #f0fff4; } + .row-slow { background: #fff5f5; } + .row-alt { background: #f7fafc; } + .row-unreachable { background: #f7fafc; color: #a0aec0; } + + /* Empty state */ + .empty-state { color: #4a5568; text-align: center; padding: 3rem 2rem; } + .empty-state p { margin: 0; } + .empty-state p:first-child { font-size: 1.1rem; margin-bottom: 0.5rem; } + .empty-state p:last-child { font-size: 0.9rem; } + + /* Utilities */ + .text-muted { color: #718096; } + .text-dimmed { color: #a0aec0; } + .text-green { color: #276749; } + .text-red { color: #c53030; } + .text-blue { color: #00539f; } + .text-sm { font-size: 0.85rem; } + .text-xs { font-size: 0.75rem; } + .nowrap { white-space: nowrap; } + .font-bold { font-weight: 600; } + .font-normal { font-weight: 400; }
diff --git a/templates/index.html b/templates/index.html index 306fb90..8bdbb51 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1,9 +1,9 @@ {% extends "base.html" %} {% block content %}