Skip to content

Make transit freshness, cancellations and station context consistent across views #401

Closed

Description

@Medformatik

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.

Remaining gaps

  • Station discovery failure reads as an empty board. Place boards first find linked stops by name. searchByNameRaw drops providers whose search fails. getLinkedStops then returns no stops, and that result is cached for TTL.placeStops. buildMergedTimetable answers a successful empty board without isPartial. Only a failure of every timetable fetch is reported as an error.
  • No provider supplies upstream age. Freshness.dataAsOf exists in the freshness contract, but the MOTIS wrappers build freshness with freshnessNow() (cloud, local) and no transit provider sets dataAsOf. Age-based fresh/stale states therefore occur only in fixtures.

Acceptance criteria

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.

Activity

  1. updated Priority Medium and Effort Medium on Oct 6, 2026
  2. Medformatik commented on Oct 7, 2026

    @Medformatik
    CollaboratorAuthor

    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.

  3. Medformatik commented on Oct 31, 2026

    @Medformatik
    CollaboratorAuthor

    #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:

    • Provider clients report failures. The stop searches of MOTIS (local and Transitous), HAFAS REST, DB, TfL and the dynamic-registry HAFAS adapter used to return [] on upstream errors. They now throw.
    • The orchestrator reports gaps. searchByNameRaw marks its result isPartial when 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.
    • Partial discovery is short-lived. It is kept for 60 seconds instead of 24 hours. Partial place responses are sent with 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:

    • Per-stop lookups. Route, alert and facility lookups for linked stops still read a provider failure as "none" (getRoutesForStop, getStopAlerts and getFacilities in the orchestrator return empty results).
    • Upstream data age. No transit provider sets Freshness.dataAsOf yet, so age-based fresh/stale states still occur only in fixtures.
    • Shared stop identity. Departures, journey and navigation views don't yet refer to the same source-aware stop/route identity, so the parent/sibling criterion is still open.
  4. Medformatik commented on Oct 31, 2026

    @Medformatik
    CollaboratorAuthor

    #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":

    • The HAFAS REST, DB, iRail, TfL, Transitland and dynamic-registry HAFAS clients throw on upstream errors instead of returning []. So do TfL alerts, Swiss alerts when all three feeds fail, and MOTIS line lookups.
    • The orchestrator tells "no source offers this lookup" apart from "the source is failing or in cooldown". In the second case, getRoutesForStop, getStopAlerts and getFacilities throw.
    • A place board marks its lines, alerts and facilities partial when some linked stops fail, and fails when all do. DB no longer claims stop alerts it doesn't have.

    Shared identity, partly (#703).

    • The merged place board no longer folds the opposite direction of a line, or the next train of a frequent line, into one row. A row absorbs another source's departure only when the line and headsign are compatible. The same source merges only on the same trip.
    • Trip lookups send each source's trip id to its own provider, so one failing source no longer hides the trip.
    • Trip detail opened from a planned journey shows the train's destination, the planned time with its delay, and the platform, cancellation and occupancy, the same way a departure board does.
    • OTP leg ids carry the otp: prefix.

    Upstream data age (#705). Boards now carry Freshness.dataAsOf where the source states it: HAFAS realtimeDataUpdatedAt (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 is null for 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:

    • Which stop a merged row comes from. A place board merges the departures of every linked stop, such as the main station, its bus stops and its platforms. Rows show a platform when the source provides one, but not which linked stop they came from. At a parent station with nearby siblings, a rider can't tell whether a bus leaves from the forecourt stop or the one around the corner.
    • Opening the exact stop from a journey. Tapping a stop in a planned journey geocodes it to the nearby place and opens that place's merged board (handleStopClick in TransitDetailsView.tsx). The leg's own stopId is 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.

  5. Medformatik commented on Nov 1, 2026

    @Medformatik
    CollaboratorAuthor

    #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:

    Not done, and not needed for the criteria:

  6. self-assigned this
    on Nov 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Fields

Priority

Medium

Effort

Medium

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions