Repository navigation
Make transit freshness, cancellations and station context consistent across views #401
Description
What problem would this solve?
Transit users need to distinguish planned times, current realtime changes, stale/partial results and the station/platform being shown. OpenMapX already models cancellation, delays, platform and stop hierarchy; the remaining proposal is coherent presentation and provenance across departures, journey and navigation views, not a new transit engine.
What would you like to be able to do?
Audit a bounded set of station/journey fixtures and apply one shared presentation policy to departure rows, linked stop views, journey legs and navigation. Expose scheduled versus realtime status and age when known, cancellation/platform changes, source/degraded coverage and clear parent/sibling station context. Reuse provider and MOTIS contracts; preserve source identity rather than coalescing similar names. Out of scope: GTFS repair (#295), entrance ingestion/routing (#298), indoor navigation, new providers or globally restyling every transit feature. Decide the supported first providers and stale/unknown thresholds from their actual timestamp semantics before implementation.
Verified current state
Rechecked on 30 October 2026 against main at fe796a6405f215fda3b18032e8181ed8647ac108.
- Existing departure status UI, departures view and stop infrastructure.
- Shared transit contract, MOTIS mapping and departure-query lifecycle.
- Journey sheet and navigation view are the cross-view consumers.
- feat(transit): clarify schedule changes and timetable failures #425 added the shared status policy and status/notice components: "Scheduled time" for timetable-only service, "Times may have changed" for known outdated predictions, and a query-level warning with Retry that keeps earlier results. Unknown upstream age stays unknown.
Remaining gaps
- Station discovery failure reads as an empty board. Place boards first find linked stops by name.
searchByNameRawdrops providers whose search fails.getLinkedStopsthen returns no stops, and that result is cached forTTL.placeStops.buildMergedTimetableanswers a successful empty board withoutisPartial. Only a failure of every timetable fetch is reported as an error. - No provider supplies upstream age.
Freshness.dataAsOfexists in the freshness contract, but the MOTIS wrappers build freshness withfreshnessNow()(cloud, local) and no transit provider setsdataAsOf. Age-based fresh/stale states therefore occur only in fixtures.
Acceptance criteria
- Scheduled-only, fresh realtime, stale/unknown realtime, partial failure, cancellation and platform-change fixtures produce consistent non-colour-only explanations. (controlled UI fixtures, feat(transit): clarify schedule changes and timetable failures #425; fresh/stale by age needs provider timestamps, see gaps)
- Departures/journey/navigation refer to the same source-aware stop/route identity; parent/sibling stations and platforms remain distinguishable.
- A cancelled service is never styled as an ordinary available departure; stale data is not claimed as live. (departure, journey and navigation views, feat(transit): clarify schedule changes and timetable failures #425)
- Traveler views do not imply that an enabled integration has working realtime coverage. (feat(transit): clarify schedule changes and timetable failures #425 decided to omit provider hosting details from traveler views; originally: "Local versus hosted provider/source context is accessible without implying that an enabled integration has working realtime coverage.")
- Loading/failure/recovery retains useful scheduled information and clear uncertainty; accessibility and small-screen readability are checked. (timetable refresh failures done in feat(transit): clarify schedule changes and timetable failures #425; station discovery failure still reads as empty)
- Source/age semantics and fallback behavior are documented, with before/after screenshots and adapter/UI regression fixtures. (public-transit docs, PR attachments and tests in feat(transit): clarify schedule changes and timetable failures #425)
Verification approach
Add adapter fixtures and rendered UI cases for each status with controlled clocks and slow/out-of-order responses. Test parent/sibling stop selection and matching route colours/IDs. Run relevant transit tests, root lint/types/test and docs build; repeat the station and small-screen cases against the same feed/provider configuration.
Dependencies and decisions
Baseline #393; related #389 (station search), #298 (entrances) and #295 (feed repair) stay separately tracked. None is a blocker for a bounded presentation pass. Timestamp semantics and initial provider/fixture set are decisions to settle in triage.
Alternatives or workarounds
A new routing engine or GTFS transformation is not necessary for consistent presentation. Inferring freshness from fetch completion or a provider name can misrepresent old upstream data.
Additional context
Area
Other / cross-cutting
Ownership and status
Maintainer-owned backlog. #425 delivered the transit status presentation. Remaining work: report station discovery failures as failed or partial instead of a cached empty board, have providers populate Freshness.dataAsOf from upstream timestamps, and confirm cross-view stop identity (criterion 2).
Related merged implementation: #425. Its UI/status fixtures do not by themselves establish complete coverage from every transit provider.
Metadata
Metadata
Assignees
Labels
Type
Fields
Priority
Effort
Activity
Medformatik commentedon Oct 7, 2026
Implemented the bounded presentation and timetable-fetch pass in #425 (
fce72ba4,2c3eb0de,2b2bb6e4,a2b8e1d1,47b902d3). The final full suite passed 17,718 tests across 1,614 files; types, lint, policy and docs passed. Five direct PR attachments demonstrate controlled desktop/phone EN/DE states; no screenshot assets were committed.This issue remains open: station name-search failures before the timetable-fetch stage can still yield a cold-cache false-empty board, and upstream publication age is unavailable from current wrappers. Missing age stays unknown. The PR lists all review limits and two deferred presentation minors. It preserves linked-board matching, identity contracts and installed-shell query ownership.
fix(transit): report failed station lookups instead of empty boards (#…
Medformatik commentedon Oct 31, 2026
#694 (
589f00e1c) fixes the station discovery gap listed under "Remaining gaps".A failed stop-name search no longer turns into an empty board that stays cached:
[]on upstream errors. They now throw.searchByNameRawmarks its resultisPartialwhen an eligible provider failed or was in health cooldown. It rejects when providers should have answered and none did. Providers excluded by the data-use policy don't count as gaps.no-store, and the linked-stop, route, alert and facility hooks refetch partial answers after a minute.The board now shows "may be incomplete", or a failure with Retry, instead of "No departures". So the criterion "Loading/failure/recovery retains useful scheduled information and clear uncertainty" is met for station discovery.
Still open:
getRoutesForStop,getStopAlertsandgetFacilitiesin the orchestrator return empty results).Freshness.dataAsOfyet, so age-based fresh/stale states still occur only in fixtures.fix(transit): report failed departures, lines, alerts and facilities (#…
fix(transit): keep separate departures apart and trip lookups on thei…
feat(transit): show how old a board's realtime data is where the sour…
Medformatik commentedon Oct 31, 2026
#702 (
68c644dd9), #703 (d64c6e95f) and #705 (1ea48b315) cover the three items left open in the previous comment.Per-stop lookups (#702). Departure, arrival, line, alert and facility lookups no longer read a provider failure as "none":
[]. So do TfL alerts, Swiss alerts when all three feeds fail, and MOTIS line lookups.getRoutesForStop,getStopAlertsandgetFacilitiesthrow.Shared identity, partly (#703).
otp:prefix.Upstream data age (#705). Boards now carry
Freshness.dataAsOfwhere the source states it: HAFASrealtimeDataUpdatedAt(dynamic registry and transport.rest; 7–38 s in live samples) and TfL prediction timestamps (44 s). DB passes the same field through, but it isnullfor now. MOTIS/Transitous, Entur, OTP, MBTA, iRail and SIRI/OJP don't state an age, and neither do Swiss boards, whose realtime comes mostly from OJP. A merged board only has an age when every realtime source on it has one. "Times may have changed" is now judged by how far behind the source was when we fetched, so our own caching and refresh intervals don't count against it.Still open, both presentation decisions under criterion 2:
handleStopClickinTransitDetailsView.tsx). The leg's ownstopIdis only used as a fallback id. So the journey and the board refer to the place, not the specific stop or platform the leg uses.A per-row realtime age (instead of one age for the board) is a possible follow-up, but not needed for the criteria.
feat(transit): name the stop a station board departure leaves from (#714
feat(transit): open a journey's stop as that stop and keep it as the …
fix(search): open a famous station as the station, with its departures (
fix(transit): show lines by the name riders know and fold repeated li…
fix(transit): link a station's stops by its local name, not a transla…
fix(transit): show platform changes and the platform riders see (#728)
fix(transit): warn of missing departures only for sources that leave …
Medformatik commentedon Nov 1, 2026
#714 (
ed9ae0fae) and #716 (11429489b) cover the two items left open under criterion 2, so this issue is done.Which stop a merged row comes from (#714). A row from a stop other than the board's main stop shows that stop's name next to its line and platform. Screen readers announce it as "Stop: …". The stop is the one the source names for the departure, otherwise the linked stop that listed it. Different spellings of one name, such as "Köln Hbf" and "Köln Hauptbahnhof", count as the same stop and get no label.
Opening the exact stop from a journey (#716). Tapping a stop in a planned journey opens that stop by the leg's source stop id, as search and map taps do. It no longer opens whatever place a name search returns. That stop is the board's main stop and stays so after a reload or in a shared link (
stop=). The navigation views don't open stops; they show the itinerary leg's own stop and route.Related fixes found while testing live boards:
Acceptance criteria:
null) don't state an age, so their boards show none. This is documented.Not done, and not needed for the criteria: