Skip to content

Add Interrail as a pass and read timetables from train-api - #83

Open
davd-gzl wants to merge 11 commits into
mainfrom
interrail-pass
Open

davd-gzl wants to merge 11 commits into
mainfrom
interrail-pass

Conversation

@davd-gzl

@davd-gzl davd-gzl commented Oct 5, 2026 •

Copy link
Copy Markdown
Collaborator

Before: the app only shows SNCF trains with a free MAX seat, read from its own daily snapshot, so an Interrail holder can't see the TGV INOUI, Intercités, night and international trains they can actually take.

After: the header's pass picker offers MAX JEUNE, MAX SENIOR and Interrail. Timetables come from train-api (refreshed daily), with the app's own snapshots as the fallback. Interrail shows every train in the SNCF feed, international stops included, and every result page says that no open data tells whether a pass-holder seat is left. "How does it work?" now says who can use each pass and how, including the special case for people living in France, and an exact trip shows its CO₂ by train against driving and flying.

Covers the "pass as a choice" and "app on the API, falling back to the static snapshot" checkboxes of #81.

Screenshots

Taken from this branch's build against the live train-api, for Monday 19 October.

Interrail: where to from Paris MAX JEUNE: same search, for comparison
Interrail selected in the pass picker: 236 destinations from Paris, including Brussels, Germany, Switzerland, Italy and Spain MAX JEUNE selected: only the trains with a free MAX seat
Interrail: exact trip Paris → Marseille Interrail on mobile, with the seat notice
Exact trip with Interrail: the 30-day calendar counts every train that day Mobile: the notice that no open data says whether a pass-holder seat is left, then the destination list

Try it

git fetch origin interrail-pass && git checkout interrail-pass
npm ci && npm run dev

Then open http://localhost:5173/?card=interrail&mode=from&from=PARIS%20(intramuros) and switch passes in the header. For the CO₂ line, open an exact trip: http://localhost:5173/?card=interrail&mode=od&from=PARIS%20(intramuros)&to=TOULOUSE%20MATABIAU.

How

  • Source. index.html names train-api in <meta name="train-api">. loadDataset reads <base>/sncf/<pass>/all.json (MAX JEUNE and MAX SENIOR both read max-jeune; Interrail reads interrail) and index.json for freshness. It falls back to the bundled snapshot when train-api is off, slow (8 s) or unreachable, and then to the sample fixture. The CSP allows offware-apps.github.io. The search worker reads the same source as the page, so its cache dump always describes the page's trains.
  • Format. Both projects use the same compact format (src/data/compact.ts): each service stored once, with a bitmask of the dates it runs. The full SNCF feed (358,954 trains) comes to 1.3 MB.
  • Fallback snapshots. fetch-data still writes tgvmax.json and now also interrail.json, so the app keeps working without train-api (constitution §5).
  • Profiles. INTERRAIL_PROFILE treats every running train as bookable, drops the MAX-only international exclusions and sets seatKnown: false. profileForCard maps the pass to its profile.
  • Loading. At boot, main.ts, the worker and the theme.js preload pick the pass from card= in the link, or else the saved pass. Switching between a MAX pass and Interrail loads the other timetable while the page stays up: the current results dim under a Loading pill until the new ones are ready, and each timetable loads once per visit, so switching back is instant. Switching between the two MAX passes re-runs straight away.
  • Copy. t() reads <key>_ir when the seat is unknown. There are new keys for the pass name, its how-to line and the seat notice. Booking still opens SNCF Connect, which sells pass-holder reservations.
  • Who can use each pass. "How does it work?" lists MAX JEUNE (16 to 27) and MAX SENIOR (60+) with the booking window and daily limits, who can buy Interrail (residents of the 33 countries, Eurail for everyone else, youth, senior and child passes) and the France residence rule (a Global Pass covers one trip out and one trip back in), with links to the official pages. The Interrail seat notice ends with that France rule. All 11 locales.
  • Ecology. An exact trip shows one quiet line: the trip's CO₂ by train against driving alone and flying, from ADEME factors (2.3, 110.6 and 224.2 g CO₂e per km) over the straight-line distance (src/util/eco.ts, numbers formatted for the locale). The README's "Supported passes" table now gives the same rules, with the CO₂ comparison under it.
  • Gates. The e2e, verify and screenshot servers strip the train-api meta, so the gates run on the bundled snapshot and never depend on the live API.
  • Docs. docs/user-flows.md, README.md, llms.txt and api.json are updated.

Checks

tsc, npm test (234 passed, including train-api and fallback tests in tests/interrail.test.ts and the CO₂ figures in tests/eco.test.ts), npm run build, npm run verify and npm run test:e2e (25/25, including a pass switch that checks the page never reloads) all pass. The new notice and CO₂ line wrap without cutting text at 390 px in English, French and German. With train-api unreachable, the app falls back to interrail.json with no CSP errors.

Refs #81, refs #92

davd-gzl and others added 2 commits October 5, 2026 05:28
Interrail holders can now pick their pass in the header. Every train in the
SNCF feed counts for them, since each takes a pass-holder reservation, and
every result says that no open data tells whether a pass-holder seat is left.

The data-refresh script now also writes public/data/interrail.json: the whole
feed in a compact format that stores each service once with a bitmask of the
dates it runs, so the full feed stays a small static file. Switching between
a MAX pass and Interrail opens the page again on the other snapshot; the
worker, the boot preload and the copy (11 locales) follow the pass.

Refs #81
@davd-gzl davd-gzl self-assigned this Oct 5, 2026
MAX JEUNE / MAX SENIOR read train-api's max-jeune file and Interrail its
interrail file. When train-api is off, slow (8 s) or unreachable, the app
reads its own snapshot as before. The worker reads the same source as the
page. The test servers strip the train-api meta so the gates run on the
bundled snapshot.
@davd-gzl davd-gzl changed the title Add Interrail as a pass choice Add Interrail as a pass and read timetables from train-api Oct 5, 2026
@davd-gzl
davd-gzl marked this pull request as ready for review October 5, 2026 07:15
…p the prerender off it

- Skip train-api when it is over 36 h old or every date has passed, unless the snapshot is out of reach too
- Bound the train-api wait to the server's answer, not the download
- Service worker: train-api timetables are network-first with an offline copy (they share the site's origin, so they were cached as static assets)
- Prerender blocks off-server requests and checks the train-api meta survives
The test needs more than 30 destinations from Paris, and a blind
today+5 days can land on a day with far fewer free MAX seats. That
made the daily Pages deploy fail since 10 October.
… and plane

How does it work? now lists the rules of each pass: MAX JEUNE (16 to 27) and MAX SENIOR (60+),
the booking window and daily limits, who can buy Interrail (residents of the 33 countries,
Eurail for everyone else) and the special case for people living in France.

An exact trip shows one line comparing its CO2 by train with driving alone and flying, using
ADEME factors over the straight-line distance (src/util/eco.ts).

Refs #92
The other pass's timetable now loads while the page stays up: the current
results dim under a Loading pill until the new search is ready, and each
timetable loads once per visit, so switching back is instant.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant